Benutzerdefinierte Felder Validierung Implementierung

This is an article for future DEVs that might need this for creating custom Plugin specifically for validation User custom fields.

Written while having Discourse version 3.6.0.beta3-latest (current commit a7326abf15), so if something won’t work it might be that something has changed in the Core code.

Main reason for writing this one is an absolute absence of information regarding adding custom validation to User custom fields.

The short story is that I had to add an custom validation for one of User custom field, which is basically an free text input. The main requirement was to make this field to have an unique value. While being Python/PHP developer, its pretty easy to implement on the frameworks/CMS that I‘m working with, while Discourse wasn’t the case and its not about the language specifics, but the fact that there no documentation for doing this and the posts that I’ve found with similar questions that might help, weren’t answered. After half week of Core research I finally achieved the expected result and I want to share it with others, as it might help for someone.

So User custom field that should have unique value.

To solve this we need to create an custom plugin for Discourse, the main reason for this is that we need to check the value in DB, which done on the backend.

Here is the structure:

- my_custom_plugin
-- assets
--- javascripts
---- discourse
----- initializers
------ your-initializer-js-file.js
-- config
--- locales
---- server.en.yml
---- client.en.yml
--- settings.yml
-- plugin.rb

Now the main issue that I faced was on how to inject into the custom fields at all. After searching the Core I’ve found and api method addCustomUserFieldValidationCallback, it can be found at plugin-api.gjs file in Core. This is an method that fires after first try of submiting the Signup form and afterwards fires on each typing in the input or any User custom field, which is what I needed.
In the example for this method, it shows that it accepts callback function and the argument that it provides you back is the userField itself that you interact with at the moment.
Here is the code for your-initializer-js-file.js (not full but enough):

import { apiInitializer } from "discourse/lib/api";
import { i18n } from "discourse-i18n";

export default apiInitializer("1.8.0", (api) => {
  // List of field IDs that require unique validation
  const UNIQUE_FIELD_IDS = ["5"];

  // Client-side validation callback
  api.addCustomUserFieldValidationCallback((userField) => {
    const fieldId = userField.field.id.toString();
    
    // Only validate fields in our unique fields list
    if (UNIQUE_FIELD_IDS.includes(fieldId)) {
      // Check if value is empty
      if (!userField.value || userField.value.trim() === "") {
        return null; // Let default validation handle empty values
      }
      
      const value = userField.value.trim();
      
      // Check against values list
      const unique = isValueUnique(fieldId, value);
      if (!unique) {
        return {
          failed: true,
          ok: false,
          reason: i18n("js.my_custom_plugin_text_validator.value_taken"),
          element: userField.field.element,
        };
      }
    }
    
    return null;
  });
});

Main points to comment here:

  • UNIQUE_FIELD_IDS is used to filter which field I want to be effected by my custom logic. The number 5 is the field ID, it can be cheked while open Edit page for the custom field within the URL.
  • isValueUnique is an custom function where all your custom logic does the validation. It should be specified outside of api.addCustomUserFieldValidationCallback.

Because we need to check the value in the DB, that means that we need to either make an API call to some endpoint that will do the validation and provide us response if its valid or not OR there might be another way to check this without an custom endpoint that I didn’t found, yet. There is an module for some validation, but its validation restricted to REGEX check right in the JS, which is not what I needed.

plugin.rb file is where we specify information regarding our custom plugin, its configs and everyhting that possible to be added. In my case I register my custom Endpoint for the validation. Here is an example of possible validation, you might have it differently.

# frozen_string_literal: true

# name: my_custom_plugin
# about: Validates uniqueness of custom user fields
# version: 0.1
# authors: Yan R

enabled_site_setting :my_custom_plugin_enabled

after_initialize do
  # Hardcoded list of unique field IDs
  UNIQUE_FIELD_IDS = ["5"].freeze
  
  # Add API endpoints for validation
  Discourse::Application.routes.append do
    get "/my_custom_plugin_endpoint_url/:field_id" => "my_custom_plugin_validation#validate"
  end

  class ::MyCustomPluginValidationController < ::ApplicationController
    skip_before_action :verify_authenticity_token
    skip_before_action :check_xhr
    skip_before_action :redirect_to_login_if_required

    def validate
      field_name = params[:field_name]
      field_value = params[:field_value]
      
      return render json: { valid: true } unless field_name.present? && field_value.present?
      return render json: { valid: true } unless field_name.start_with?("user_field_")
      
      # Normalize the value: trim whitespace and compare case-insensitively
      normalized_value = field_value.to_s.strip
      return render json: { valid: true } if normalized_value.empty?

      existing = UserCustomField
        .where(name: field_name)
        .where("LOWER(TRIM(value)) = LOWER(?)", normalized_value)
        .exists?

      render json: { valid: !existing }
    end

Now here is the tricky part, you can use this endpoint and make fetch/ajax call from your initializer, but it won’t work, the main reason addCustomUserFieldValidationCallback doesn’t work with async callbacks, so you need to make call that won’t be async. I used XMLHttpRequest for this, as an example xhr.open('GET', '/my_custom_plugin_endpoint_url/${fieldId}', true); where true disables async.
This will start working, but you will face an issue, where you cannot quickly type in the input, because addCustomUserFieldValidationCallback fires with each provided letter and will wait till it have an proper return, so it freeze your input.

I didn’t show it in the examples, but my solution was to retrieve the field unique values list after page load and use it to filter the value straight in the JS without making additional API Calls, you just need to make sure that you have reasonable amount of values and secure the endpoint. In my case instead of the values list, I returned hashes list, so in JS I turn the typed value into hash and compare hashes. Beside the fact that it a bit more secure, it also reduce the response wieght significally, so you can grab much more values for compare.

Here the infomration regarding the config folder.

settings.yml:

plugins:
  my_custom_plugin_enabled:
    default: true
    client: true

server.en.yml

en:
  site_settings:
    my_custom_plugin_enabled: "Enable unique user fields validation"
  my_custom_plugin_text_validator:
    value_taken: "This value is already taken"

client.en.yml

en:
  js:
    my_custom_plugin_text_validator:
      value_taken: "This value is already taken"

Addition: I didn’t found information how to to add my custom plugin beside adding it in app.yml, as I have Docker setup. What I did is added my custom plugin in the docker folder/repo, which contains the setup of Discourse and mounted it while Rebuild operation into the plugin folder within the Container.

Hope this will help someone to implement custom validations for the User fields.

1 „Gefällt mir“