GJS component fails with "undefined helper" error when shown via modal service (Discourse 3.5.0

encountering a persistent rendering error when trying to display a Glimmer component (.gjs) as a modal using this.modal.show(). The modal is triggered from another GJS component added to the post menu via the post-menu-buttons value transformer. I’m running Discourse v3.5.0.beta3-dev

I am trying to Add a button to the post menu using api.registerValueTransformer("post-menu-buttons", ...). Clicking this button should open a modal defined by a separate GJS component (FeedbackFormModal) using this.modal.show(FeedbackFormModal, ...).

When the button is clicked and this.modal.show() is called, the application crashes with the following error, seemingly during the rendering process of the FeedbackFormModal

Error occurred:

- While rendering:
  -top-level
    application
      (unknown template-only component)
        DiscourseRoot
          ModalContainer
            FeedbackFormModal
              DModal
                conditional-in-element:ConditionalInElement
                  (unknown template-only component) index.js:3970:18

Error occurred: index.js:3377:16

Uncaught (in promise) Error: Attempted to use a value as a helper, but it was not an object or function. Helper definitions must be objects or functions with an associated helper manager. The value was: 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 // Line number might differ slightly
    _triggerAction d-button.gjs:138
    Ember 10

This happens even when the FeedbackFormModal template is stripped down to its absolute minimum, containing only imported core components (<DModal>, <DButton>) and standard built-in helpers (if, on, etc.).

Pasting the code below for reference:

plugin.rb

# frozen_string_literal: true
# name: my-plugin

module ::MyPlugin
  # ... constants ...
end

require_relative "lib/my_plugin/engine"

after_initialize do
  # Load Dependencies (using require_dependency and 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__)
  # ... other dependencies ...

  # Add methods to User (using class_eval as prepend failed)
  ::User.class_eval do
    # Define helper methods like my_custom_stat, etc.
    def my_custom_stat; # ... implementation ...; end
    public :my_custom_stat
    # ... other methods ...
  end

  # Prepend Guardian Extensions (if any)
  # ::Guardian.prepend(MyPlugin::GuardianExtensions)

  # Serializer modifications
  reloadable_patch do |plugin|
    # Add attributes to serializers, e.g.:
    add_to_serializer(:post, :some_flag_for_button) do
        # Logic to determine if button should show
        true # Example
    end
    # ... other serializer additions ...
  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"; // Button component
// ... import other components for other outlets ...

export default apiInitializer("1.13.0", (api) => {
  // Use Value Transformer for Post Menu Button
  api.registerValueTransformer("post-menu-buttons", ({ value: dag, context }) => {
    const { post } = context;
    // Logic to determine if button should render based on post.some_flag_for_button
    const shouldRenderButton = post?.some_flag_for_button; // Example flag

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

  // ... renderInOutlet for other UI elements ...
});

Button Component (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"; // The modal component

export default class LeaveFeedbackButton extends Component {
  @service modal;
  @service appEvents; // Used for error display

  // Args: post

  get buttonLabel() { return "Action Button"; } // Hardcoded
  get buttonTitle() { return "Perform Action"; } // Hardcoded

  @action
  openFeedbackModal() {
    console.log("Opening Modal...");
    try {
       // Simplified model for testing
       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" {{!-- Example icon --}}
      @label={{this.buttonLabel}}
      title={{this.buttonTitle}}
    />
  </template>
}

Modal Component (assets/javascripts/discourse/components/feedback-form-modal.gjs - Ultra-Simplified)

import Component from "@glimmer/component"; // Ensure this is imported
// Removed tracked, action, service etc. if not needed by simplified version
import DModal from "discourse/components/d-modal"; // Ensure this is imported
import DButton from "discourse/components/d-button"; // Ensure this is imported
import { on, preventDefault } from '@ember/modifier'; // Import built-ins if used

export default class FeedbackFormModal extends Component {
  // Minimal JS needed for simplified template

  // Example getter needed by template
  get modalTitle() { return "My Modal Title"; } // Hardcoded
  get cancelLabel() { return "Cancel"; }
  get submitLabel() { return "Submit"; }

  // Dummy action if needed by button
  @action submitFeedback() { console.log("Dummy submit"); }

  {{!-- ULTRA-SIMPLIFIED TEMPLATE THAT STILL CAUSES ERROR --}}
  <template>
    <DModal @title={{this.modalTitle}} @closeModal={{@closeModal}} class="feedback-form-modal">
      <:body>
          <p>--- MINIMAL MODAL TEST ---</p>
          {{#if this.errorMessage}} {{!-- Using built-in '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"}}> {{!-- Using built-in 'if' --}}
              {{this.submitLabel}}
          </DButton>
      </:footer>
    </DModal>
  </template>
}

Given that the error Attempted to use a value as a helper... undefined persists even when rendering an ultra-simplified GJS component template (containing only imported core components like <DModal>/<DButton> and built-in helpers like if) via this.modal.show() triggered from a component added via registerValueTransformer("post-menu-buttons", ...), what could be causing this?

Is there a known issue or limitation with helper/component scope resolution in modals triggered this way in recent Discourse versions (specifically 3.5.0.beta3-dev)? Are there alternative recommended patterns for showing modal forms from post menu buttons in GJS?

Any pointers would be greatly appreciated!

1 Like

I tried your minimal test code and it works for me:

The only changes I made is to add a missing action import, removing a comment outside the template and forcing the button to show.

Can you make a quick TC on GitHub that triggers this error?

1 Like

Thank you for taking a look. After your confirmation I was trying to setup a github TC with a fresh plugin and I ended up isolating the problem myself.

It occurs when using the if helper within a component argument (@icon={{if...}}) inside the modal template. Removing this specific usage makes the modal render correctly.

This is what I was trying to do @icon={{if this.isSubmitting "spinner"}}

BTW I love this new Horizon theme.

1 Like

Hello,

Following up on this issue: I created the minimal-modal-test plugin as suggested.

  • Success: When the test plugin’s modal component (minimal-modal.gjs) contained only basic elements (like <p>, <textarea>, core <DModal>, <DButton>, and even built-in helpers like #if for conditional blocks or if used for the submit button’s icon), it rendered correctly without errors when opened via modal.show() triggered from the button component added via the post-menu-buttons value transformer. This confirms the basic modal service, component rendering, value transformer, and built-in helpers seem functional in isolation in my environment (Discourse v3.5.0.beta3-dev).

  • Failure: The error (TypeError: userProvidedCallback is undefined / previously Attempted to use a value as a helper... undefined) reappears consistently the moment I add the child component <StarRatingInput> back into the modal’s template (minimal-modal.gjs or feedback-form-modal.gjs).

I have ensured the StarRatingInput component itself uses workarounds identified earlier (inline SVG instead of dIcon helper, arrow functions for methods like starClass to fix this context, standard JS Array.from instead of the range helper). The internal code of StarRatingInput seems correct now.

This points to the error being triggered by rendering the nested <StarRatingInput> component (which contains an #each loop, dynamic class bindings, and event handlers) inside the parent modal component within this specific rendering context.

Steps to Reproduce

  1. You are running the latest Discourse development environment version.

  2. Log in as any user.

  3. Navigate to any topic and view the first post.

  4. Click the “Test Failing Modal” button added to the post menu.

  5. Open the browser’s developer console.

Expected Result

A simple modal dialog should appear containing a star rating input.

Actual Result

The modal fails to render completely. The browser console shows an error.

1 Like

Thanks for the repo, it helped!
I found your issue. It’s not about the StartRatInput component, it’s actually because you import eq from the wrong path.

It fails here:

checked={{eq this.testRating ratingValue}}

And you need to change the import:

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

import { eq } from "truth-helpers";

Result:

3 Likes

Thank you so much for this! Half the time I have wasted struggling with imports. :roll_eyes:

1 Like