Join the #BuildWithBuildAgent Challenge! Get recognized, earn exclusive swag, and inspire the ServiceNow Community with what you can build using Build Agent.  Join the Challenge.

Amarjeet Pal
Kilo Sage
Kilo Sage

 

M2M table in catalog builder (Wizard)


There’s a helpful article on the ServiceNow Community about this, but here’s a quick summary using the Service Offering example:

  • When a Service Offering is selected, the related list Available for Subscribers on the catalog item should be populated.
  • This behavior can vary based on requirements like User Criteria.

To achieve this:

  1. Create two Catalog Wizard Questions:

    • One for selecting the Service Offering.
    • One for populating the related list.
  2. Mapping Difference:

    • The first question should have map to set = false.
    • The second should have map to set = true.
  3. Technical Setup:

    • The first field must be a List Collector.
    • It should be placed on a Variable Set.
    • Ensure the OOTB Catalog OnChange client script is active on the Variable Set—this is essential for populating the related list dynamically.



function onChange(control, oldValue, newValue, isLoading) {
 var CATALOG_QUESTION_NAME =Service offering variable name;
 var CATALOG_PRODUCER_NAME = M2M producer set name ;
 var CATALOG_PRODUCER_QUESTION_NAME = 'service_offering';
 
 if (isLoading) {
  g_form.setDisplay(CATALOG_PRODUCER_NAME, false);
  convertRowValuesToCommaSeparatedString(CATALOG_QUESTION_NAME, CATALOG_PRODUCER_NAME, CATALOG_PRODUCER_QUESTION_NAME);
  return;
 }
 
 var valueArr = !newValue ? [] : newValue.split(',');
 
 addRows(CATALOG_PRODUCER_NAME, CATALOG_PRODUCER_QUESTION_NAME, valueArr);
}
 
 
//TODO: move to UI scripts and use it in here.
function addRows(producerSetName, producerSetQuestionName, includedValues) {
    var recordMap = getRecordMap(producerSetName, producerSetQuestionName);
    var ans = [];
    for (var i = 0; i < includedValues.length; i++) {
        var row = {};
        row[producerSetQuestionName] = includedValues[i];
  if (recordMap && recordMap[includedValues[i]])
   row['sys_id'] = recordMap[includedValues[i]];
 
        ans.push(row);
    }
 
    g_form.setValue(producerSetName, JSON.stringify(ans));
 
}
 
function convertRowValuesToCommaSeparatedString(questionName, producerSetName, producerQuestionName) {
    var value = g_form.getValue(producerSetName);
 
    try {
        value = JSON.parse(value);
    } catch (e) {
        value = [];
    }
 
    var ans = [];
    for (var i = 0; i < value.length; i++) {
        var row = value[i];
        if (row && row.hasOwnProperty(producerQuestionName))
            ans.push(row[producerQuestionName]);
    }
 
    g_form.setValue(questionName, ans.join());
}
 
function getRecordMap(producerSetName, producerQuestionName) {
 
    var fieldObj = g_form.$private.getField(producerSetName);
 
    if (fieldObj._recordMap)
        return fieldObj._recordMap;
 
    var originalValue = fieldObj.originalValue || '[]';
 
    var originalJSON;
    try {
        originalJSON = JSON.parse(originalValue);
    } catch (e) {
        originalJSON = [];
    }
 
    var recordMap = {};
    for (var i = 0; i < originalJSON.length; i++) {
        var obj = originalJSON[i];
        if (obj && obj[producerQuestionName] && obj['sys_id'])
            recordMap[obj[producerQuestionName]] = obj['sys_id'];
    }
    fieldObj._recordMap = recordMap;
    return fieldObj._recordMap;
}

 

 

This is a major step that isn’t documented. Everything else follows the out-of-the-box behavior—try to replicate it similar to how "Available for" (User Criteria ) works.

 

Thanks
Amarjeet Pal