特定のページにコンテンツを追加したい場合、最適な方法は plugin-outlet(プラグイン・アウトレット)を使用することです。簡単に言うと、plugin-outlet は Discourse のテンプレートに予約されたスペースで、ここに新しいコンテンツを追加できます。
まず初めに、ターゲットとするページに plugin-outlet が存在するかどうかを確認する必要があります。その際に役立つテーマコンポーネントがあります。
(deprecated) Plugin outlet locations theme component
このコンポーネントをインストールし、有効化したら、ターゲットとするページに移動して利用可能なアウトレットを確認してください。今回の場合、plugin-outlet が存在します(緑色でハイライト)。
したがって、必要なものは above-user-profile です。
もしこれが存在しなかったらどうなるでしょうか?その場合の最善策は、追加を依頼するか、コアに追加するための PR(プルリクエスト)を送ることです。ユースケースが妥当であれば、ほとんどの場合承認されます。
とにかく、今回のケースでは既に存在しています。では、どのようにマークアップを追加するか見ていきましょう。残りの手順では上記のコンポーネントは不要なので、plugin outlet の名前がわかった時点で無効にできます。
必要なことは、テーマのヘッダータブに以下のようなコードを追加することだけです。
<script type="text/x-handlebars" data-template-name="/connectors/OUTLET_NAME/SOME_NAME">
ここにマークアップを記述...
</script>
OUTLET_NAME をターゲットとする outlet の名前に変更し、SOME_NAME をこのカスタマイズに与えたい名前に変更してください。名前は任意ですが、可能であれば説明的な名前にするのが良い慣習です。これにより、以下のようなコードになります。
<script type="text/x-handlebars" data-template-name="/connectors/above-user-profile/add-profile-videos">
ここにマークアップを記述...例えば
<h1>Hello World!!</h1>
</script>
これを試して結果を見てみましょう… 覚えておいてください、上記のスニペットはテーマの common > header タブに記述します。
そして…
ここまでは順調ですが、さらに深く掘り下げてみましょう。
すべてのプロフィールで動画を表示したくはないはずです。ある条件に基づいてのみ表示したいでしょう。では、どうすればよいでしょうか?そのためには、2 つの要素が必要です。消費するデータと、少しの JavaScript です。
データを探しましょう。plugin-outlet が予約されたスペースだと述べたことを思い出してください。では、文脈(コンテキスト)なしにそれらを設ける意味は何でしょうか?そのため、Discourse は関連するコンテキストの断片を各 plugin outlet に渡します… まず一歩引いて考えましょう。以下のようなコードを追加すると
<script type="text/x-handlebars" data-template-name="/connectors/above-user-profile/add-profile-videos">
ここにマークアップを記述...例えば...
<h1>Hello World!!</h1>
</script>
これは HTML のように見えます(script タグ自体は HTML ですが)、その中身は handlebars コードとして扱われます。
つまり、以下のような処理も可能です。
<script type="text/x-handlebars" data-template-name="/connectors/above-user-profile/add-profile-videos">
{{log this}}
</script>
そしてブラウザのコンソールを確認してください。Outlet がレンダリングされるたびに、つまりユーザーページにいるときに、以下のような出力が見られます。
これらの情報が役立つかというと、現時点ではそうではありません。後で戻りましょう。もう一歩引いて、Discourse がどのようにコンテキストを outlet に渡しているか見てみましょう。GitHub またはローカル環境で outlet 名を検索すると、以下のような結果が得られます。
Repository search results · GitHub
そのファイルを開いてください。最初に表示されるのはこの行です。
その行の最後の部分を見てみましょう
args=(hash model=model)
Discourse が model を引数として outlet に渡していることがわかります。実用上の目的と単純化のため、model = data と考えて問題ありません。
つまり、outlet の引数の一つが model であり、欲しいデータはそこに入っています。では、スニペットに戻りましょう。
<script type="text/x-handlebars" data-template-name="/connectors/above-user-profile/add-profile-videos">
{{log this}}
</script>
これを以下のように変更します
<script type="text/x-handlebars" data-template-name="/connectors/above-user-profile/add-profile-videos">
- {{log this}}
+ {{log args}}
</script>
これでコンソールに以下のような出力が得られます。
そのデータを参照して、必要なものが含まれているか確認してください。そのページ上の他の要素で使用されているユーザーに関するすべてのデータが含まれているため、含まれているはずです。これは、その特定のユーザーのユーザーページの「モデル」です。
利用可能なプロパティの一つは… ドラムロール
… ユーザーが所属するグループです。
したがって、以下のようにすると
{{log args.model.groups}}
コンソールにユーザーが所属するすべてのグループが表示されます。
さて、必要なデータが手に入ったので、残っているのはそれに基づいて条件を追加することだけです。
同じスニペット内で処理できると考えたいかもしれませんが、残念ながらそれはできません。Handlebars はテンプレート言語であり、ロジックのサポートは非常に、非常に 限定的です。単純な真偽条件とループ以上のことはできません。比較処理などは行えません。
では、どこでそれを行えばよいのでしょうか?「connector class(コネクタークラス)」です。少し派手に聞こえますね… 分かります。
簡単に言うと、connector class は outlet に付随する JavaScript の一部です。実際にはもっと複雑ですが、現時点ではそれだけ知っていれば十分です。
では、作成してみましょう。以下のようにします
<script type="text/discourse-plugin" version="0.8">
api.registerConnectorClass('OUTLET_NAME', 'SOME_NAME', {
});
</script>
ここで OUTLET_NAME と SOME_NAME は上記で使用したものと同じである必要があります。したがって、変更しましょう
<script type="text/discourse-plugin" version="0.8">
api.registerConnectorClass('above-user-profile', 'add-profile-videos', {
});
</script>
このスニペットもテーマの common > header タブに記述します。これで以下のような状態になっているはずです
<script type="text/x-handlebars" data-template-name="/connectors/above-user-profile/add-profile-videos">
{{log args.model.groups}}
</script>
<script type="text/discourse-plugin" version="0.8">
api.registerConnectorClass('above-user-profile', 'add-profile-videos', {
});
</script>
connector class の内部で処理を行えますが… 注意が必要です。これは単なる JavaScript ファイルとは異なります。より適切な表現が見つからないため… 痩身した Ember コンポーネントだと考えてください。これ以上の詳細は範囲外ですので、先に進みましょう。
デフォルトで 4 つのメソッドが接続されています
actions を使用して、以下のようにアクションを定義できます
api.registerConnectorClass("above-user-profile", "add-profile-videos", {
actions: {
myAction() {
// 何かを行う
}
}
});
その後、ボタンが押された際などに、outlet 内でそのアクションを呼び出すことができます。今回は必要ありませんので、先に進みましょう。
api.registerConnectorClass("above-user-profile", "add-profile-videos", {
shouldRender(args, component) {
// ここで true または false を返す
}
});
これも使用しません。outlet はプロフィールページでのみレンダリングされ、現時点では他の要件がないためです。ただし、outlet がレンダリングされる前にテストしたい条件(現在のユーザーの信頼レベルなど)を追加するために使用できます。次に…
api.registerConnectorClass("above-user-profile", "add-profile-videos", {
setupComponent(args, component) {
// 何かを行う
}
});
これが焦点を当てるべきメソッドです。設定したい JavaScript の条件や変数はここに入ります。このメソッドをさらに掘り下げる前に、完全性の観点から最後のメソッドを説明しましょう
api.registerConnectorClass("above-user-profile", "add-profile-videos", {
teardownComponent(args, component) {
// 何かを行う
}
});
これは outlet が削除されるときに発火します。イベントリスナーの削除など、必要なクリーンアップを行うことができます。
さて、setupComponent に戻りましょう
api.registerConnectorClass("above-user-profile", "add-profile-videos", {
setupComponent(args, component) {
// 何かを行う
}
});
2 つの引数が渡されていることがわかります。まず args、次に component です。
ここで args は先ほど見たものと同じです。Discourse が outlet に渡したコンテキストデータです。したがって、以下のようにすると
api.registerConnectorClass("above-user-profile", "add-profile-videos", {
setupComponent(args, component) {
console.log(args.model.groups);
}
});
先ほどブラウザのコンソールで見たのと同じ情報、つまりプロフィール所有者が所属するグループが表示されます。ここからが本番です。データも正しいフックも手に入りました。したがって、ここで好きな処理を行えます。つまり、特定のグループに所属するメンバーのプロフィールでのみ動画を表示したい場合、以下のようにできます
api.registerConnectorClass('above-user-profile', 'add-profile-videos', {
setupComponent(args, component) {
const inGroup = [...args.model.groups].filter(g => g.name === TARGET_GROUP)
const showVideo = inGroup.length ? true : false;
console.log(showVideo);
}
});
staff グループに所属するユーザーのプロフィールページでこれを試すと、コンソールに true が出力されます。これで残っているのは、それを outlet テンプレートに渡すことです。その方法は以下の通りです。
setupComponent に渡される component は、connector と outlet で共有されます。component のプロパティとして設定することで、outlet に値を渡すことができます
const TARGET_GROUP = "staff"
api.registerConnectorClass('above-user-profile', 'add-profile-videos', {
setupComponent(args, component) {
const inGroup = [...args.model.groups].filter(g => g.name === TARGET_GROUP)
const showVideo = inGroup.length ? true : false;
- console.log(showVideo);
+ component.setProperties({showVideo})
}
});
これで、テンプレートに戻って以下のような処理を行います
{{log showVideo}}
同じ結果が出力されます。これを Handlebars の条件文に入れ、その中にマークアップを追加します
<script
type="text/x-handlebars"
data-template-name="/connectors/above-user-profile/add-profile-videos"
>
{{#if showVideo}}
<video playsinline autoplay muted loop id="myVideo" poster="[INSERT LINK]">
<source src="[INSERT LINK]" type="video/webm">
<source src="[INSERT LINK]" type="video/mp4">
</video>
{{/if}}
</script>
staff ユーザーのプロフィールページを確認してください。動画が読み込まれることが確認できるはずです。
staff メンバーのプロフィールから移動すると、動画は消えます。staff グループに所属しないユーザーのプロフィールでは動画は表示されません。
では、これらすべてをまとめましょう。上記と同じ内容です。
以下は使用した CSS です。common > css タブ
#myVideo {
position: fixed;
top: var(--header-offset);
min-height: 100vh;
left: 0;
z-index: -1;
}
.user-content {
background: none;
}
.user-main {
padding: 0.5em;
background: rgba(var(--secondary-rgb), 0.8);
}
// モバイル版でも表示させたい場合
.mobile-view {
body[class*="user-"] {
background: none;
.user-main,
.user-content {
padding: 0.5em;
background: rgba(var(--secondary-rgb), 0.8);
}
}
}
HTML / JavaScript / Handlebars。これはテーマの common > header タブに記述します
<script
type="text/x-handlebars"
data-template-name="/connectors/above-user-profile/add-profile-videos"
>
{{#if showVideo}}
<video playsinline autoplay muted loop id="myVideo" poster="[INSERT LINK]">
<source src="[INSERT LINK]" type="video/webm">
<source src="[INSERT LINK]" type="video/mp4">
</video>
{{/if}}
</script>
<script type="text/discourse-plugin" version="0.8">
const TARGET_GROUP = "staff"
api.registerConnectorClass('above-user-profile', 'add-profile-videos', {
setupComponent(args, component) {
const inGroup = [...args.model.groups].filter(g => g.name === TARGET_GROUP)
const showVideo = inGroup.length ? true : false;
component.setProperties({showVideo})
}
});
</script>
TARGET_GROUP をターゲットとするグループ名に変更し、動画の src 属性を追加してください。
この記事は長めになりました… 気にしないでください。概念を理解すれば、上記の作業はすべて 3〜5 分以内に完了します。
良い点は、ここで話したことはほぼすべての plugin outlet に当てはまることです。変わるのは名前だけです。つまり、将来行うあらゆる plugin-outlet の修正に応用できます。
- outlet 名を見つける
- データを取得する
- connector でデータを処理する
- プロパティをテンプレートに渡す