Skip to content

Commit 707a912

Browse files
DEV: Make outletArgs available as regular arguments in PluginOutlet (#32742)
Historically, plugin outlet arguments had to be accessed like `@outletArgs.foo` in templates, or `this.args.outletArgs.foo` in javascript. Following this commit, outletArgs will be passed to connectors at the top level, so they can be accessed like `@foo` in templates or `this.args.foo` in JS. `@outletArgs` remains available for compatibility, and there are no plans to remove it. For custom classic component connectors, these new arguments may clash with functions/fields defined on the component class. This rare case will be automatically detected, protected against, and a deprecation will be shown.
1 parent d86b4ff commit 707a912

File tree

4 files changed

+116
-5
lines changed

4 files changed

+116
-5
lines changed

app/assets/javascripts/discourse/app/components/plugin-outlet.gjs

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import Component from "@glimmer/component";
22
import { cached } from "@glimmer/tracking";
3+
import ClassicComponent from "@ember/component";
34
import { concat } from "@ember/helper";
45
import { get } from "@ember/object";
56
import { getOwner } from "@ember/owner";
67
import { service } from "@ember/service";
8+
import curryComponent from "ember-curry-component";
79
import { or } from "truth-helpers";
810
import PluginConnector from "discourse/components/plugin-connector";
911
import PluginOutlet from "discourse/components/plugin-outlet";
@@ -124,6 +126,29 @@ export default class PluginOutletComponent extends Component {
124126
);
125127
}
126128

129+
@bind
130+
safeCurryComponent(component, args) {
131+
if (component.prototype instanceof ClassicComponent) {
132+
for (const arg of Object.keys(args)) {
133+
if (component.prototype.hasOwnProperty(arg)) {
134+
deprecated(
135+
`Unable to set @${arg} on connector for ${this.args.name}, because a property on the component class clashes with the argument name. Resolve the clash, or convert to a glimmer component.`,
136+
{
137+
id: "discourse.plugin-outlet-classic-args-clash",
138+
}
139+
);
140+
141+
// Build a clone of `args`, without the offending key, while preserving getters
142+
const descriptors = Object.getOwnPropertyDescriptors(args);
143+
delete descriptors[arg];
144+
args = Object.defineProperties({}, descriptors);
145+
}
146+
}
147+
}
148+
149+
return curryComponent(component, args, getOwner(this));
150+
}
151+
127152
<template>
128153
{{~#if (this.connectorsExist hasBlock=(has-block))~}}
129154
{{~#if (has-block)~}}
@@ -135,9 +160,16 @@ export default class PluginOutletComponent extends Component {
135160

136161
{{~#each (this.getConnectors hasBlock=(has-block)) as |c|~}}
137162
{{~#if c.componentClass~}}
138-
<c.componentClass
139-
@outletArgs={{this.outletArgsWithDeprecations}}
140-
>{{yield}}</c.componentClass>
163+
{{#let
164+
(this.safeCurryComponent
165+
c.componentClass this.outletArgsWithDeprecations
166+
)
167+
as |CurriedComponent|
168+
}}
169+
<CurriedComponent
170+
@outletArgs={{this.outletArgsWithDeprecations}}
171+
>{{yield}}</CurriedComponent>
172+
{{/let}}
141173
{{~else if @defaultGlimmer~}}
142174
<c.templateOnly
143175
@outletArgs={{this.outletArgsWithDeprecations}}

app/assets/javascripts/discourse/app/services/deprecation-warning-handler.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export const CRITICAL_DEPRECATIONS = [
3131
"discourse.mobile-view",
3232
"discourse.mobile-templates",
3333
"discourse.component-template-overrides",
34+
"discourse.plugin-outlet-classic-args-clash",
3435
];
3536

3637
if (DEBUG) {

app/assets/javascripts/discourse/app/static/dev-tools/plugin-outlet-debug/args-table.gjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export default class ArgsTable extends Component {
4343
window[`arg${globalI}`] = value;
4444
/* eslint-disable no-console */
4545
console.log(
46-
`[plugin outlet debug] \`@outletArgs.${key}\` saved to global \`arg${globalI}\`, and logged below:`
46+
`[plugin outlet debug] \`@${key}\` saved to global \`arg${globalI}\`, and logged below:`
4747
);
4848
console.log(value);
4949
/* eslint-enable no-console */
@@ -53,7 +53,7 @@ export default class ArgsTable extends Component {
5353

5454
<template>
5555
{{#each this.renderArgs as |arg|}}
56-
<div class="key"><span class="fw">{{arg.key}}</span>:</div>
56+
<div class="key"><span class="fw">@{{arg.key}}</span>:</div>
5757
<div class="value">
5858
<span class="fw">{{arg.value}}</span>
5959
<a

app/assets/javascripts/discourse/tests/integration/components/plugin-outlet-test.gjs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Component from "@glimmer/component";
2+
import ClassicComponent from "@ember/component";
23
import templateOnly from "@ember/component/template-only";
34
import { hash } from "@ember/helper";
45
import { getOwner } from "@ember/owner";
@@ -1056,3 +1057,80 @@ module(
10561057
});
10571058
}
10581059
);
1060+
1061+
module(
1062+
"Integration | Component | plugin-outlet | argument currying",
1063+
function (hooks) {
1064+
setupRenderingTest(hooks);
1065+
1066+
test("makes arguments available at top level", async function (assert) {
1067+
extraConnectorComponent(
1068+
"test-name",
1069+
<template>
1070+
<span class="glimmer-test">{{@arg1}} from glimmer</span>
1071+
</template>
1072+
);
1073+
1074+
await render(
1075+
<template>
1076+
<PluginOutlet
1077+
@name="test-name"
1078+
@outletArgs={{hash arg1="Hello world"}}
1079+
/>
1080+
</template>
1081+
);
1082+
assert.dom(".glimmer-test").hasText("Hello world from glimmer");
1083+
});
1084+
1085+
test("makes arguments available at top level in classic components", async function (assert) {
1086+
extraConnectorComponent(
1087+
"test-name",
1088+
class extends ClassicComponent {
1089+
<template>
1090+
<span class="classic-test">{{this.arg1}} from classic</span>
1091+
</template>
1092+
}
1093+
);
1094+
1095+
await render(
1096+
<template>
1097+
<PluginOutlet
1098+
@name="test-name"
1099+
@outletArgs={{hash arg1="Hello world"}}
1100+
/>
1101+
</template>
1102+
);
1103+
assert.dom(".classic-test").hasText("Hello world from classic");
1104+
});
1105+
1106+
test("guards against name clashes in classic components", async function (assert) {
1107+
extraConnectorComponent(
1108+
"test-name",
1109+
class extends ClassicComponent {
1110+
get arg1() {
1111+
return "overridden";
1112+
}
1113+
1114+
<template>
1115+
<span class="classic-test">{{this.arg1}} from classic</span>
1116+
</template>
1117+
}
1118+
);
1119+
1120+
await withSilencedDeprecationsAsync(
1121+
"discourse.plugin-outlet-classic-args-clash",
1122+
async () => {
1123+
await render(
1124+
<template>
1125+
<PluginOutlet
1126+
@name="test-name"
1127+
@outletArgs={{hash arg1="Hello world"}}
1128+
/>
1129+
</template>
1130+
);
1131+
}
1132+
);
1133+
assert.dom(".classic-test").hasText("overridden from classic");
1134+
});
1135+
}
1136+
);

0 commit comments

Comments
 (0)