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