DiscourseにEmber Componentsを追加する

前のチュートリアルでは、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 で提案してください。

「いいね!」 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