GJSコンポーネントがモーダルサービス経由で表示されると「undefined helper」エラーが発生する (Discourse 3.5.0)

this.modal.show() を介して Glimmer コンポーネント (.gjs) をモーダルとして表示しようとすると、持続的なレンダリングエラーが発生しています。モーダルは、post-menu-buttons 値トランスフォーマーを介して投稿メニューに追加された別の GJS コンポーネントからトリガーされます。Discourse v3.5.0.beta3-dev を実行しています。

api.registerValueTransformer("post-menu-buttons", ...) を使用して投稿メニューにボタンを追加しようとしています。このボタンをクリックすると、this.modal.show(FeedbackFormModal, ...) を使用して別の GJS コンポーネント (FeedbackFormModal) によって定義されたモーダルが開くはずです。

ボタンがクリックされ、this.modal.show() が呼び出されると、FeedbackFormModal のレンダリングプロセス中に発生しているように見える次のエラーでアプリケーションがクラッシュします。

エラーが発生しました:

- レンダリング中:
  -トップレベル
    アプリケーション
      (不明なテンプレート専用コンポーネント)
        DiscourseRoot
          ModalContainer
            FeedbackFormModal
              DModal
                conditional-in-element:ConditionalInElement
                  (不明なテンプレート専用コンポーネント) index.js:3970:18

エラーが発生しました: index.js:3377:16

未処理 (Promise 内) Error: 値をヘルパーとして使用しようとしましたが、オブジェクトまたは関数ではありませんでした。ヘルパー定義は、関連付けられたヘルパーマネージャーを持つオブジェクトまたは関数である必要があります。値は次のとおりでした:undefined
    Ember 2
    source chunk.8d6366d70b85d1b69fdc.d41d8cd9.js:89256
    Ember 2
    source chunk.8d6366d70b85d1b69fdc.d41d8cd9.js:90066
    Ember 4
    source chunk.8d6366d70b85d1b69fdc.d41d8cd9.js:90164
    source chunk.8d6366d70b85d1b69fdc.d41d8cd9.js:89224
    Ember 2
    source chunk.8d6366d70b85d1b69fdc.d41d8cd9.js:90163
    Ember 2
    source chunk.8d6366d70b85d1b69fdc.d41d8cd9.js:90284
    Ember 2
    source chunk.8d6366d70b85d1b69fdc.d41d8cd9.js:92117
    Ember 12
    source chunk.8d6366d70b85d1b69fdc.d41d8cd9.js:94577
    source chunk.8d6366d70b85d1b69fdc.d41d8cd9.js:96288
    Ember 34
    show modal.js:73
    openFeedbackModal leave-feedback-button.js:102 // 行番号は若干異なる場合があります
    _triggerAction d-button.gjs:138
    Ember 10

このエラーは、FeedbackFormModal テンプレートが、インポートされたコアコンポーネント (<DModal>, <DButton>) と標準の組み込みヘルパー (if, on など) のみを含むように最小限に削減された場合でも発生します。

参照用にコードを以下に貼り付けます。

plugin.rb

# frozen_string_literal: true
# name: my-plugin

module ::MyPlugin
  # ... 定数 ...
end

require_relative "lib/my_plugin/engine"

after_initialize do
  # 依存関係のロード (require_dependency と File.expand_path を使用)
  require_dependency File.expand_path('app/controllers/my_plugin/my_controller.rb', __dir__)
  require_dependency File.expand_path('app/models/my_plugin/my_model.rb', __dir__)
  # ... その他の依存関係 ...

  # User にメソッドを追加 (prepend が失敗したため class_eval を使用)
  ::User.class_eval do
    # my_custom_stat などのヘルパーメソッドを定義
    def my_custom_stat; # ... 実装 ...; end
    public :my_custom_stat
    # ... その他のメソッド ...
  end

  # Guardian 拡張機能のプリペンド (もしあれば)
  # ::Guardian.prepend(MyPlugin::GuardianExtensions)

  # シリアライザーの変更
  reloadable_patch do |plugin|
    # シリアライザーに属性を追加、例:
    add_to_serializer(:post, :some_flag_for_button) do
        # ボタンを表示するかどうかを決定するロジック
        true # 例
    end
    # ... その他のシリアライザーの追加 ...
  end
end

assets/javascripts/discourse/initializers/my-plugin-outlets.js

import { apiInitializer } from "discourse/lib/api";
import { hbs } from "ember-cli-htmlbars";
import LeaveFeedbackButton from "../components/leave-feedback-button"; // ボタンコンポーネント
// ... 他のアウトレットの他のコンポーネントをインポート ...

export default apiInitializer("1.13.0", (api) => {
  // Post Menu Button に Value Transformer を使用
  api.registerValueTransformer("post-menu-buttons", ({ value: dag, context }) => {
    const { post } = context;
    // post.some_flag_for_button に基づいてボタンをレンダリングするかどうかを決定するロジック
    const shouldRenderButton = post?.some_flag_for_button; // 例のフラグ

    if (shouldRenderButton) {
      dag.add("leaveMyPluginFeedback", LeaveFeedbackButton, {
        after: "like",
        args: { post: post },
      });
    }
    return dag;
  });

  // ... 他の UI 要素の renderInOutlet ...
});

ボタンコンポーネント (assets/javascripts/discourse/components/leave-feedback-button.gjs)

import Component from "@glimmer/component";
import { action } from "@ember/object";
import { service } from "@ember/service";
import DButton from "discourse/components/d-button";
import FeedbackFormModal from "./feedback-form-modal"; // モーダルコンポーネント

export default class LeaveFeedbackButton extends Component {
  @service modal;
  @service appEvents; // エラー表示に使用

  // Args: post

  get buttonLabel() { return "Action Button"; } // ハードコーディング
  get buttonTitle() { return "Perform Action"; } // ハードコーディング

  @action
  openFeedbackModal() {
    console.log("Opening Modal...");
    try {
       // テスト用の簡略化されたモデル
       const modelData = { post_id: this.args.post.id };
       this.modal.show(FeedbackFormModal, { model: modelData });
    } catch(e) {
        console.error("Error showing modal", e);
        this.appEvents.trigger("show:error", "Error opening modal.");
    }
  }

  <template>
    <DButton
      class="btn-default my-plugin-btn"
      @action={{this.openFeedbackModal}}
      @icon="star" {{!-- 例のアイコン --}}
      @label={{this.buttonLabel}}
      title={{this.buttonTitle}}
    />
  </template>
}

モーダルコンポーネント (assets/javascripts/discourse/components/feedback-form-modal.gjs - 超簡略化版)

import Component from "@glimmer/component"; // これがインポートされていることを確認してください
// 簡略化版で必要ない場合は、tracked, action, service などを削除
import DModal from "discourse/components/d-modal"; // これがインポートされていることを確認してください
import DButton from "discourse/components/d-button"; // これがインポートされていることを確認してください
import { on, preventDefault } from '@ember/modifier'; // 使用する場合は組み込みをインポート

export default class FeedbackFormModal extends Component {
  // 簡略化されたテンプレートに必要な最小限の JS

  // テンプレートで必要な例のゲッター
  get modalTitle() { return "My Modal Title"; } // ハードコーディング
  get cancelLabel() { return "Cancel"; }
  get submitLabel() { return "Submit"; }

  // ボタンで必要なダミーアクション
  @action submitFeedback() { console.log("Dummy submit"); }

  {{!-- エラーが発生する超簡略化されたテンプレート --}}
  <template>
    <DModal @title={{this.modalTitle}} @closeModal={{@closeModal}} class="feedback-form-modal">
      <:body>
          <p>--- MINIMAL MODAL TEST ---</p>
          {{#if this.errorMessage}} {{!-- 組み込み 'if' を使用 --}}
              <div class="alert alert-error" role="alert">{{this.errorMessage}}</div>
          {{/if}}
      </:body>
      <:footer>
          <DButton @action={{@closeModal}} class="btn-flat"> {{this.cancelLabel}} </DButton>
          <DButton @action={{this.submitFeedback}} class="btn-primary" @icon={{if this.isSubmitting "spinner"}}> {{!-- 組み込み 'if' を使用 --}}
              {{this.submitLabel}}
          </DButton>
      </:footer>
    </DModal>
  </template>
}

registerValueTransformer("post-menu-buttons", ...) を介して追加されたコンポーネントからトリガーされる this.modal.show() を使用して、 <DModal>/<DButton> のようなインポートされたコアコンポーネントと if のような組み込みヘルパーのみを含む超簡略化された GJS コンポーネントテンプレートをレンダリングする場合でも、Attempted to use a value as a helper... undefined というエラーが発生し続けることを考えると、何が原因である可能性がありますか?

最近の Discourse バージョン (特に 3.5.0.beta3-dev) では、このようにトリガーされたモーダルでのヘルパー/コンポーネントスコープ解決に既知の問題または制限はありますか? GJS で投稿メニューボタンからモーダルフォームを表示するための代替推奨パターンはありますか?

何か手がかりがあれば幸いです!

「いいね!」 1

最小限のテストコードを試しましたが、正常に動作しました。

変更したのは、不足していたactionのインポートを追加し、テンプレート外のコメントを削除し、ボタンを表示するように強制しただけです。

このエラーを発生させるGitHub上の簡単なTCを作成してもらえますか?

「いいね!」 1

ご確認いただきありがとうございます。確認後、GitHubのTCを新規プラグインで設定しようとしましたが、自分で問題を特定することができました。

これは、モーダルテンプレート内のコンポーネント引数(@icon={{if...}})でifヘルパーを使用すると発生します。この特定の使用例を削除すると、モーダルは正しくレンダリングされます。

これは私がやろうとしていたことです @icon={{if this.isSubmitting \"spinner\"}}

ちなみに、この新しいHorizonテーマは素晴らしいです。

「いいね!」 1

こんにちは。

この問題について、ご提案いただいた通り minimal-modal-test プラグインを作成しました。

  • 成功: テストプラグインのモーダルコンポーネント(minimal-modal.gjs)に基本的な要素(<p><textarea>、コアの <DModal><DButton>、条件付きブロックに使用される #if のような組み込みヘルパー、送信ボタンのアイコンに使用される if など)のみが含まれていた場合、post-menu-buttons 値トランスフォーマー経由で追加されたボタンコンポーネントからトリガーされる modal.show() によって開かれたときに、エラーなく正しくレンダリングされました。これは、私の環境(Discourse v3.5.0.beta3-dev)で、基本的なモーダルサービス、コンポーネントレンダリング、値トランスフォーマー、および組み込みヘルパーが単独で機能していることを確認するものです。

  • 失敗: モーダルのテンプレート(minimal-modal.gjs または feedback-form-modal.gjs)に子コンポーネント <StarRatingInput> を再度追加した瞬間に、エラー(TypeError: userProvidedCallback is undefined / 以前は Attempted to use a value as a helper... undefined)が一貫して再発します

StarRatingInput コンポーネント自体には、以前特定された回避策(dIcon ヘルパーの代わりにインライン SVG、this コンテキストを修正するための starClass のようなメソッドのアロー関数、range ヘルパーの代わりに標準の JavaScript Array.from)を使用していることを確認しました。StarRatingInput の内部コードは、現在正しいように見えます。

これは、この特定のレンダリングコンテキスト内で、親モーダルコンポーネント内にネストされた <StarRatingInput> コンポーネント(#each ループ、動的なクラスバインディング、イベントハンドラーが含まれています)をレンダリングすることによってエラーがトリガーされていることを示唆しています。

再現手順

  1. 最新の Discourse 開発環境バージョンを実行している。

  2. 任意のユーザーとしてログインする。

  3. 任意のトピックに移動し、最初の投稿を表示する。

  4. 投稿メニューに追加された 「Test Failing Modal」 ボタンをクリックする。

  5. ブラウザの開発者コンソールを開く。

期待される結果

シンプルなモーダルダイアログに、スターレーティング入力が表示される。

実際の結果

モーダルが完全にレンダリングされず失敗する。ブラウザコンソールにエラーが表示される。

「いいね!」 1

リポジトリありがとうございます、助かりました!
問題の原因を見つけました。StartRatInputコンポーネントの問題ではなく、eq を間違ったパスからインポートしていることが原因です。

ここで失敗します:

checked={{eq this.testRating ratingValue}}

そして、インポートを変更する必要があります:

import { eq } from "@ember/helper";

import { eq } from "truth-helpers";

結果:

「いいね!」 3

ありがとうございます!インポートで時間を無駄にすることがよくあります。:roll_eyes:

「いいね!」 1

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.