Custom data from GJS composer form not in `opts` for `:topic_created` event

This is in continuation to this- Custom topic fields per category or custom topic entry form per category?

I decided to create the plugin. Form component itself is rendering correctly via `api.renderInOutlet`. However the data is not getting saved on the backend in the custom table.

Frontend Setup:

  1. I’m using api.renderInOutlet("composer-fields", MyOutletConnectorComponent) to render a GJS component (MyOutletConnectorComponent) when a new topic is being created in a designated category.

  2. This MyOutletConnectorComponent then renders my main GJS form component (MyMarketFormComponenent).

  3. Inside MyMarketFormComponenent, user input updates a JavaScript object which is then set as a top-level property on the composerModel provided by the outlet arguments. For example:

    // Inside MyMarketFormComponenent action
    const currentData = this.args.composerModel.get("market_listing_data") || {};
    const updatedData = { ...currentData, [fieldKey]: newValue };
    this.args.composerModel.set("market_listing_data", updatedData); 
    
  4. In my plugin initializer (tecenc-market-composer.js), I’m using api.serializeOnCreate("market_listing_data", "market_listing_data"); to try and pass this data to the backend. I also use api.serializeToDraft("market_listing_data", "market_listing_data");.

Backend Problem:

In my plugin.rb, within the DiscourseEvent.on(:topic_created) do |topic, opts, user| ... end handler:

  • I check if the topic is in the correct category (this works).
  • I then try to access the serialized data using market_data = opts[:market_listing_data] (or opts["market_listing_data"]).
  • The issue is that opts[:market_listing_data] is consistently nil.
  • Logging opts.keys confirms that my market_listing_data key is not present at the top level of the opts hash. The opts hash itself contains standard keys like :raw, :title, :category, :archetype, etc.

My plugin.rb snippet for the event:

# plugin.rb
# ...
module ::TecencMarket
  SERIALIZED_DATA_KEY = "market_listing_data".freeze
end
# ...
after_initialize do
  # ...
  DiscourseEvent.on(:topic_created) do |topic, opts, user|
    # ... (category check logic) ...

    # This is where market_data is nil
    market_data = opts[TecencMarket::SERIALIZED_DATA_KEY.to_sym] 

    Rails.logger.info "[MyPlugin] Opts keys: #{opts.keys.inspect}"
    Rails.logger.info "[MyPlugin] market_data from opts: #{market_data.inspect}"

    if market_data.present? && market_data.is_a?(Hash)
      # ... process and save data to custom table ...
    else
      Rails.logger.warn "[MyPlugin] Market data not found in opts as expected."
    end
  end
  # ...
end

Question:

  1. What is the correct and most reliable way to ensure that a JavaScript object set as a top-level property on the composerModel (e.g., composerModel.set("my_plugin_data_key", { ... });) using GJS components is properly serialized by api.serializeOnCreate and made available in the opts hash of the :topic_created event on the backend?

  2. Is api.serializeOnCreate("my_key", "my_key") expected to work for arbitrary top-level keys set on the composerModel from a GJS context, or is serializing via custom_fields (e.g., setting composerModel.custom_fields.my_plugin_key = {...} and using api.serializeOnCreate("custom_fields.my_plugin_key")) the only robust/recommended approach for this kind of data transfer for new topics?

  3. Are there any specific considerations or common pitfalls when using api.serializeOnCreate with data managed by GJS components that might cause the data not to be included in the opts hash?

I’ve confirmed the frontend form is collecting data correctly and setting it on the composerModel.market_listing_data property. The issue seems to be purely in getting this data serialized and passed to the backend event.

Any guidance or examples of the recommended pattern for this in modern Discourse would be greatly appreciated!

Thanks!

Hi again,

Following up on my previous issue about data not reaching the opts hash, I’ve shifted my strategy to use the recommended custom_fields pathway for serializing data from my plugin’s composer form.

Current Approach:

  1. Frontend Initializer:
    • I’m attempting to ensure composerModel.custom_fields and composerModel.custom_fields["tecenc_market_data"] (my plugin’s data key) are initialized as objects. My latest attempt involves using api.modifyClass("model:composer", ...) to add initialization logic to init() and clearState():

      // In my plugin initializer (tecenc-market-composer.js)
      api.modifyClass("model:composer", {
        pluginId: "tecencMarketInitCustomFieldsOnComposer",
        init() {
          this._super(...arguments); 
          if (!this.custom_fields || typeof this.custom_fields !== 'object') {
            this.custom_fields = {};
          }
          if (!this.custom_fields["tecenc_market_data"] || typeof this.custom_fields["tecenc_market_data"] !== 'object') {
            this.custom_fields["tecenc_market_data"] = {};
          }
        },
        clearState() {
          this._super(...arguments); 
          if (!this.custom_fields || typeof this.custom_fields !== 'object') {
            this.set("custom_fields", {});
          }
          let cf = this.get("custom_fields"); 
          if (!cf["tecenc_market_data"] || typeof cf["tecenc_market_data"] !== 'object') {
            const newCustomFields = { ...cf }; 
            newCustomFields["tecenc_market_data"] = {};
            this.set("custom_fields", newCustomFields);
          }
        }
      });
      
    • I then use api.serializeOnCreate('custom_fields.tecenc_market_data'); and api.serializeToDraft('custom_fields.tecenc_market_data');.

    • My GJS form component updates composerModel.custom_fields.tecenc_market_data.my_field = value; (by setting a new custom_fields object for reactivity).

  2. Backend plugin.rb:
    • Topic.register_custom_field_type("tecenc_market_data", :json) is called.
    • The :topic_created event handler expects to read data from topic.custom_fields["tecenc_market_data"].

New Blocking Error:

Despite these attempts to initialize custom_fields on the composerModel, I am now consistently encountering the following JavaScript error when trying to open the composer for a new topic (by clicking the “Create Topic” button):


Uncaught (in promise) Error: Property set failed: object in path "custom_fields" could not be found.

Ember 3

open composer.js:1010

open composer.js:1009

_setModel composer.js:1451

open composer.js:1401

openNewTopic composer.js:1410

createTopic list.js:167

clickCreateTopicButton d-navigation.gjs:224

_triggerAction d-button.gjs:138

Ember 11

_triggerAction d-button.gjs:135

click d-button.gjs:93

source chunk.8d6366d70b85d1b69fdc.d41d8cd9.js:94366

Ember 2

source chunk.8d6366d70b85d1b69fdc.d41d8cd9.js:94040

Ember 26 property_set-BapAkp3X.js:79:10

This error indicates that this.model.custom_fields (on the composerModel) is undefined or null when core Discourse code at composer.js:1010 (within the open method, called by openNewTopic) attempts to set a nested property on it. This happens before my plugin’s form can even be rendered.

Questions:

  1. Given the core Composer model should initialize custom_fields: {}, why might composerModel.custom_fields still be undefined at composer.js:1010 during the openNewTopic sequence in this Discourse version?
  2. Is the api.modifyClass("model:composer", ...) approach in an initializer the correct/most effective way to ensure custom_fields (and nested plugin keys within it) are initialized as objects early enough to prevent this core error? If not, what is the recommended pattern?
  3. Could there be a timing issue where core composer initialization steps are running and expecting custom_fields before plugin initializers using api.modifyClass have fully taken effect on the Composer model prototype or instances?

Any insights into this “Property set failed” error in the composer’s core and the best practices for plugins to safely interact with composerModel.custom_fields during initialization would be extremely helpful.

Thanks for your time!

I think you need to add your data to the serializer on the rails side.

Is your data getting in to rails? Where are you storing it?

Look at plugins that have addToSerializer.

I am saving the data in a custom database table.

addToSerializer is for moving data from server to client, isnt it?

My problem is moving data from client to server so that I can save it. So I am using serializeOnCreate. But the problem is that the data is not flowing to the opts hash. So, currently, the answer is no, the custom data isn’t making it into the opts hash as expected. Its not getting into rails.

I did look into this educational plugin but not able to make it work for 6 custom fields.

1 Like

Got it to work.

I was missing this step: api.serializeToTopic(MY_BLOB_KEY, \topic.${MY_BLOB_KEY}\);

2 Likes