Preventing Duplicates in Combined Fields (Many-to-Many Records) Using JavaScript (Solved)

Seems a common request in Knack is to prevent users from submitting duplicate records in a join object—like a Student registering for the same Club more than once. This post shows how to enforce uniqueness to multiple connected objects without workflows or third-party tools like Integromat.

Instead, generate a Unique ID inside the form based on the two connected objects via JS. This field is then enforced as unique by the Knack database.


:brain: What this solves

This approach prevents duplicate join records in many-to-many relationships by constructing a UniqueID field from the IDs of two connected records - one from each object.

Example:

You have:

Students (object_A)

Clubs (object_B)

Registrations (join object) — connects Students and Clubs

You want to stop a student from joining the same club twice.


:puzzle_piece: How it works

  1. On the Student page, each student has a list of their Registrations.

  2. The “Add Registration” link opens a form where you pick a Club from a dropdown.

  3. The Student ID is already known from the view context.

  4. Add JavaScript to combine the Student and Club record IDs into the UniqueID field on the form before submission

  5. If a duplicate is attempted, the built-in Knack validation blocks submission.


:hammer_and_wrench: What you need

• A join object (e.g. Registrations) with:

• A connection to Students (field ID = field_AAA)

• A connection to Clubs (field ID = field_BBB)

• A short text field for UniqueID (field ID = field_CCC)

• The form where the user creates a Registration:

  • Club field is a dropdown

  • UniqueID field is on the form and locked via JS. While it shouldn’t be hidden, it can be rendered effectively invisible via CSS.

• All field codes (AAA/BBB/CCC) can be found in the Data Model section of the builder.

• The form view ID (e.g. view_XXX) can be found in the URL of the builder when viewing the form.

:white_check_mark: Add this JavaScript to your app

$(document).on('knack-view-render.view_XXX', function(event, view) {
  console.log("🎯 view_XXX rendered");

  const $clubField = $('#view_XXX-field_BBB');
  const $uniqueInput = $('#field_CCC');
  const studentId = Knack.hash_id;

  // Update: StudentID field doesn't need to appear on the form so commented out
  // const $studentField = $('#view_XXX-field_AAA').closest('.kn-input');
  // $studentField.hide();

  // Lock and visually minimize UniqueID field
  $uniqueInput
    .prop('readonly', true)
    .on('focus touchstart', function(e) {
      e.preventDefault();
      $(this).blur();
    })
    .css({
      'background-color': '#f0f0f0',
      'cursor': 'not-allowed',
      'pointer-events': 'none',
      'font-size': '1px',
      'width': '1px',
      'padding': '0',
      'border': 'none'
    });

  // Generate UniqueID when Club is selected
  function updateUniqueIdField() {
    const clubId = $clubField.val();
    console.log("updateUniqueIdField running", { studentId, clubId });

    if (studentId && clubId) {
      const uniqueId = `${studentId}_${clubId}`;
      console.log("Setting UniqueID to:", uniqueId);
      $uniqueInput.val(uniqueId).trigger('change');
    }
  }

  updateUniqueIdField();
  $clubField.on('change', updateUniqueIdField);
});

// Reapply locking behavior if the form submission fails
$(document).on('knack-form-submit.error.view_XXX', function() {
  setTimeout(() => {
    $('#field_CCC')
      .prop('readonly', true)
      .on('focus touchstart', function(e) {
        e.preventDefault();
        $(this).blur();
      });
  }, 10);
});

// Optionally replace system error message if UniqueID isn't unique
const observer = new MutationObserver(() => {
  const $error = $('.kn-message.is-error');
  if ($error.length && $error.text().includes("UniqueID must be unique")) {
    console.warn("🩹 Error message replaced via MutationObserver");
    $error.text("This appears to be a duplicate.");
  }
});
observer.observe(document.body, { childList: true, subtree: true });

:warning: Notes

Do not hide the UniqueID field on the form, or Knack won’t register updates. Locking and styling it is a safer workaround.

• You must set the UniqueID field in the database to Unique = Yes to enforce the constraint.


:speech_balloon: Final thoughts

This method is lightweight, works entirely in-browser, and doesn’t rely on external services. It’s not perfect—but so far it’s working reliably in my app.

1 Like