Discourse に Ember コンポーネントを追加する

前のチュートリアルでは、Discourseのサーバー側とクライアント側の両方を設定してリクエストに応答する方法を紹介しました。

現在、Emberコンポーネントのドキュメントを読むことをお勧めします: Introducing Components - Components - Ember Guides

コンポーネントを作成する前に、共有コンポーネント、ヘルパー、モディファイアを含むUIキットを確認してください。ほとんどの一般的なコントロールはすでにそこに存在します。

古いチュートリアル

このチュートリアルでは、サードパーティのJavaScriptをラップする方法として、新しいEmberコンポーネントを作成します。これは、以前に作成したYouTubeビデオに似ており、参考になるかもしれませんが、今回はDiscourse特有の内容であり、プロジェクト内でファイルを配置する方法に焦点を当てています。

なぜコンポーネントを使うのか?

Handlebarsは、かなりシンプルで魅力的な言語です。通常のHTMLに動的な部分を加えたものです。学習しやすく生産性にも優れていますが、コードの再利用性にはやや劣ります。Discourseのような大規模なアプリケーションを開発している場合、同じものを何度も再利用したくなることに気づくでしょう。

コンポーネントは、Emberがこの問題に対する解決策です。スナックをより美しく表示するコンポーネントを作成してみましょう。

新しいコンポーネントの作成

コンポーネントの名前にはダッシュ(-)を含める必要があります。今回は fancy-snack を名前に選びます。テンプレートを作成しましょう:

app/assets/javascripts/admin/templates/components/fancy-snack.hbs

<div class="fancy-snack-title">
  <h1>{{snack.name}}</h1>
</div>

<div class="fancy-snack-description">
  <p>{{snack.description}}</p>
</div>

次に、このコンポーネントを使用するために、既存の admin/snack テンプレートを以下で置き換えます:

app/assets/javascripts/admin/templates/snack.hbs

{{fancy-snack snack=model}}

これで、必要なモデルを渡すだけで、他のテンプレートでも fancy-snack コンポーネントを再利用できます。

カスタムJavaScriptコードの追加

再利用性に加えて、Emberのコンポーネントは、カスタムJavaScript、jQuery、その他の外部コードを安全に追加するのにも最適です。コンポーネントがページに挿入されるタイミングと、削除されるタイミングを制御できます。これを行うには、いくつかのコードを含むEmber.Componentを定義します:

app/assets/javascripts/admin/components/fancy-snack.js

export default Ember.Component.extend({
  didInsertElement() {
    this._super();
    this.$().animate({ backgroundColor: "yellow" }, 2000);
  },

  willDestroyElement() {
    this._super();
    this.$().stop();
  },
});

上記のコードを追加してページをリフレッシュすると、スナックがゆっくりと黄色い背景にフェードインするアニメーションを持っていることがわかります。

ここでは何が起きているのかを説明しましょう:

  1. コンポーネントがページにレンダリングされると、didInsertElement が呼び出されます。

  2. didInsertElement(および willDestroyElement)の最初の行は this._super() です。これは、Ember.Componentをサブクラス化しているため必要です。

  3. アニメーションは、jQueryのanimate 関数を使用して行われます。

  4. 最後に、コンポーネントがページから削除される際に呼び出される willDestroyElement フックで、アニメーションがキャンセルされます。

なぜ willDestroyElement を気にするのか疑問に思うかもしれません。その理由は、Discourseのような長期間動作するJavaScriptアプリケーションでは、メモリリークを引き起こしたり、処理が走りっぱなしになったりしないよう、後片付けをすることが重要だからです。ここではアニメーションを停止することで、コンポーネントがページに表示されなくなったため、jQueryのタイマーはそれ以上発火しなくてよいことを伝えています。

次のステップ

このシリーズの最終チュートリアルでは、自動テストについて説明しています。


このドキュメントはバージョン管理されています - 変更提案はGitHubで行ってください。

「いいね!」 18

Hi, how do i extend a discourse component thru a plugin? Can you give me some points. Thanks

「いいね!」 1

Generally we prefer you don’t extend Discourse plugins, and you stick to plugin outlets or using the widget decoration API to add stuff.

But if you must, you can create an initializer and use Ember’s extend code. Here’s an example that extends an Ember object.

「いいね!」 4

Tried with initializer but didnt worked. What i actually want to do is to add 2 more classNames and some actions:

import { withPluginApi } from 'discourse/lib/plugin-api';

function initializeComponentTopicList(api) {
  // extend component from jsapp/components/topic-list.js.es6
  const TopicList = api.container.lookupFactory('component:topic-list');

  TopicList.extend({
    classNames: ['topic-list', 'round', 'table'],
    actions: {
        clickMe: function() {
            console.log('click');
        }
    }
  });
};

export default {
  name: "extend-for-component-topic-list",

  initialize() {
    withPluginApi('0.1', initializeComponentTopicList);
  }
};

And by “didnt worked”, i mean that topic list completly disappeared from page.
Thank you

Were there any logs in the console?

Nope, no logs at all. However i managed to fix it this way. I hope it will help someone.

import { default as TopicList } from 'discourse/components/topic-list';
import { withPluginApi } from 'discourse/lib/plugin-api';

function initializeComponentTopicList(api) {
  TopicList.reopen({
    classNames: ['topic-list', 'round', 'table'],
  });
};

export default {
  name: "extend-for-component-topic-list",

  initialize() {
    withPluginApi('0.1', initializeComponentTopicList);
  }
};
「いいね!」 3

I just ran into the same problem. Add the following as a css/html customisation and observe empty user cards:

<script type="text/discourse-plugin" version="0.5">
    api.container.lookupFactory('component:user-card-contents')
</script>
「いいね!」 2

ここに更新されたドキュメントがあると便利です。これはglimmerコンポーネントのドキュメントを指すことができます。

「いいね!」 3