Important changes to Plugin Outlets for Ember 2.10

Shortly I will be merging a branch of Discourse into master that updates us to the latest stable version of Ember, which is 2.10. This is exciting because we’ve been behind Ember’s stable branch for a while and we’ve finally caught up! It also contains the Glimmer 2 template engine which is quite a bit faster so the application should be more responsive, and it also cuts down on our template file sizes so the app should be quicker to download.

However, there is a downside, and that I wasn’t able to get plugin outlets to work with 100% backwards compatibility due to internal changes in Ember.

What’s changed with Plugin Outlets

Previously, if your plugin made a connector to a {{plugin-outlet}}, you’d have the exact same scope in the handlebars document as where the outlet was declared. In the latest release, you will only have access to variables that are explicitly passed in to the outlet. I’ve gone through every plugin outlet in the codebase and passed in the main models and variables so if you were using those you should be OK. However, if you were using unusual or rare variables from the template you might have to update your code.

The most likely thing to break in your plugins is if you added data to a template by extending a controller and not a model, as they won’t be passed into your connector anymore. Fortunately, there’s a straightforward way to fix that:

New in Discourse: Connector Classes

You can now optionally define a class that will be associated with your connector.

Let’s say your connector was defined in my-plugin/templates/connectors/user-profile-controls/my-connector.hbs. You could create the following class:

my-plugin/connectors/user-profile-controls/my-connector.js.es6

export default {
  setupComponent(args, component) {
    component.set('today', new Date());
  }
}

When your connector is inserted to the outlet, it will call the setupComponent method and you will get access to the plugin’s arguments (such as models) and you can call set on it to make variables available in your template.

A new feature is you can define a method called shouldRender which will determine if the connector will be inserted into the template. This is useful because previously all connectors had to be inserted, which made working with <ul> and <li> tags complicated:

my-plugin/connectors/user-profile-controls/my-connector.js.es6

export default {
  // if false the plugin won't be rendered
  shouldRender(args, component) {
     return component.siteSettings.my_plugin_enabled;
  }
}

Finally, you can also declare actions in your component class. This is much simpler than using an initializer to extend the controller and defining the actions there. If you have added any actions to controllers you’ll need to move them into component classes.

my-plugin/connectors/user-profile-controls/my-connector.js.es6

export default {
  actions: {
    myAction() {
      console.log('my action triggered');
    }
  }
}

Upgrade Schedule

I’ve been reviewing all the officially supported plugins as well as many popular plugins to make sure they can be updated for this change. Once I’m confident that everything is working I’ll be merging this into the master / tests-passed branches of Discourse. This should happen either this afternoon or first thing Tomorrow morning. After that, we’ll keep a close eye on it before we merge it into our next beta.

The change is due to be in our next stable release, which should be available in early January.

The faster you upgrade your plugins, the better! If you need some help or find a bug in a common plugin just let me know and I’ll fix it as soon as possible.

18 Likes

Update: We’re just going to deploy for meta for now. Ember 2.10 is running here so please let us know if you notice anything broken.

If you are feeling adventurous, you can deploy from the ember-2.10 branch on your discourses and try things out yourself!

7 Likes

Is it possible to create a connector class from a site customization?

Not yet. There’s an internal way to register a class but it’s not exposed via plugin api yet.

Will it be exposed via the plugin api before ember-2.10 lands on master? I’d quite like to keep my user card/profile customizations as a site customization (rather than a full-blown-plugin), if possible

I’m afraid it’s too late. @eviltrout just merged it.

Give me a few minutes, I’ll add it to the plugin api.

3 Likes

Okay I’ve addeed a Plugin API (version 0.6) to register a connector class. If you’re writing a full plugin I recommend you don’t use this and let the connector class be automatically resolved, but if you’re using a customization this will work:

https://github.com/discourse/discourse/commit/250ca114167ca10701cd1dded33f2f4b37ac29a3

Sample:

api.registerConnectorClass('user-profile-primary', 'my-connector', {
  shouldRender(args, component) {
    return component.siteSettings.my_plugin_enabled;
  }
});
8 Likes

Just to expand a bit, looks like if you want to set a class on wrapping component you would use:

api.registerConnectorClass('user-profile-primary', 'my-connector', {
  setupComponent() {
    this.set('classNames', ['foo']);
  }
});

With the new changes I am seeing a wrapping span @eviltrout which is a bit of a blocker for me:

Outlet is defined as:

 {{plugin-outlet name="user-activity-bottom"
                    connectorTagName='li'
                    args=(hash model=model)}}

Is there any way to remove the wrapping span?

1 Like

Unfortunately there’s no way to remove it. Maybe you can change the HTML to have two ul tags, where the second one is the outlet?

Unlike the posted example directory structure

I noticed that some plugins (eg, cakeday) are like

my-plugin/assets/javascripts/discourse/connectors/user-profile-controls/my-connector.js.es6

I’m guessing the directory structure is not important as long as the folder is named “connectors” and the choice is up to preference.

I am wondering how to differentiate between different but identically named plugin outlets. i.e.
“topic-list-tags”
app/assets/javascripts/discourse/templates/list/topic-list-item.raw.hbs
app/assets/javascripts/discourse/templates/mobile/list/topic-list-item.raw.hbs

“discovery-list-container-top”
app/assets/javascripts/discourse/templates/discovery.hbs
app/assets/javascripts/discourse/templates/tags/show.hbs

2 Likes

Yeah I realized a bunch of plugins did it one way or the other so both are supported. In the long term I’d like to standardize on one and warn plugins that do it the other way.

In those cases the outlet is effectively the same - you can’t see desktop or mobile at the same time, and you can’t see a tag/show as well as discovery.

If your plugin needs to be more specific, we might have to add another outlet.

3 Likes

I’m trying to port a plugin to work with the Ember 2.10 port… however it seems topic-list-tags outlets actually don’t get output at all? I thought the context field didn’t exist anymore but the code tells me otherwise, however even if I’m using inline templates from customizations nothing seems to change in my topic list:

<script type='text/x-handlebars' data-template-name='/connectors/topic-list-tags/topic-test.raw'>
<div>
test?
</div>
</script>

… yet, this does not show up in my actual topic list - if I remove the .raw I do get the usual error that would happen if a non-raw template gets used instead of a raw template (e.template being undefined), but this topic-list-tags connector doesn’t work from a plugin either.

@eviltrout - did you even verify the functioning of your new ‘raw’ plugin outlets? :slight_smile:

2 Likes

This should fix it:

3 Likes

After the upgrade, my plugin is not able to route the URI to the correct template.Are there any changes in the Ember routing as well?

I am using this Ember route code to map this URI www.example.com/home/page with the template main-page.hbs located in the home folder

export default {
    resource: 'home',
    path: '/home',
    map() {
        this.route('main-page', { path: 'page' });

    }
};

git hub link to route file @route
Full source code for the plugin is available @github

Any leads or hints will be highly appreciated.

I am not sure how that worked before - I think perhaps for a very short period of time we had a home resource, but that no longer exists. Since you are not embedding your route inside another one, you can just export a function and it should work:

export default function() {
  this.route('main-page', { path: '/home/page' });
}
4 Likes

Thanks, somehow I was confused with the resource part

I was unable to get actions working like it says above, but I found another way to do it:

In OP:

export default {
  actions: {
    myAction() {
      console.log('my action triggered');
    }
  }
}

What I did:

export default {
  setUpComponent(args, component) {
    component.set('actions', {})
    component.set('actions.myAction', () => { console.log('my action triggered') })
  } 
}

That seems incorrect. How were you triggering your action?

In the handlebars file:

{{input type="checkbox" name="myCheckbox" id=group.name
        change=(action "myAction")}}

It seems to work–is there a better/more correct way to do it?