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

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

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

古いチュートリアル

このチュートリアルでは、サードパーティの 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 で行えます。

「いいね!」 17

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

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