YUCEL
YUCEL

Talking about my Salesforce.com and MuleSoft experience

Edgar Moran
Author

Software Engineer, passionate for tech stuff video and photography, Salesforce and Mulesoft developer

Share


Our Newsletter


Subscribe to get new post notifications.

* indicates required

Tags


Twitter


YUCEL

How to create a custom merge field template with Apex

Edgar MoranEdgar Moran

During many implementation we have used the standard functionality of merge templates in Salesforce, for example one we want to configure an email template we just have to select the field related to the object and automatically salesforce paste the API name in the text area we are working on. But what about to have some procedure or process that needs to have merge fields, and plus that to have it dynamic based on an specific Sobject, so for that I had to create my own merge field template page and logic in order to achieve it.

So what are the main parts I have to implement:

  • A template builder. Basically this is how my template is created based on a main Object, metadata fields and finally a dynamic way to put those fields in a text area with a singular format to recognize those when I want to substitute them.

  • In the other side we must to have the traductor that basically is how we read that template stored in salesforce to transform it in data, let's say we have something like this:

TEMPLATE:

"Hello {contact.Name}, welcome to yucelmoran.com!"

TRADUCTOR:
"Hello Yucel Moran, welcome to yucelmoran.com"

So now, I'll try to explain a little bit each part. (do not forget to leave me a comment in case you think I could do something better [plus the language :P])

For this also I've used the financial force Metadata API listed in this repo (https://github.com/financialforcedev/apex-mdapi) and also pretty good samples in the Andy in the Cloud Blog https://andyinthecloud.com

  1. Retrieve Salesforce Objects with the metadata API.

For this and in order to make it easier, I'm using remote apex:

function load_objects(){

				Visualforce.remoting.Manager.invokeAction(
			        '{!$RemoteAction.myclass.loadObjects}',
			        function(result, event){
			            if (event.status) {			            	
			            	var data = result.replace(/(&quot\;)/g,"\"");
			            	//console.log('data'+data);
			            	var information = jQuery.parseJSON(data);
			            	var options = $("#options");
			            	options.append($("<option />").val('Selecciona').text('Selecciona'));
			            	$.each(information, function (index, value) {
			            		options.append($("<option />").val(value.name).text(value.label));
			            		$.unblockUI();

							});
			               	            	
            
			            } 
			            else if (event.type === 'exception') {
			               console.log('exception'+event.message);
			            } 
			            else {
			               console.log('other error'+event.message);
			            }
			        }, 
			        {escape: true}
			    );
			}

@RemoteAction
    public static String loadObjects(){
        List<ObjectDescriber> objdescriber_list   = new List<ObjectDescriber>();
        Schema.DescribeSObjectResult[] descResult = Schema.describeSObjects(new String[]{'Account','Contact'});

        for(Schema.DescribeSObjectResult schema_result:descResult){
            ObjectDescriber object_meta_description = new ObjectDescriber();
            object_meta_description.label = schema_result.getLabel();
            object_meta_description.name  = schema_result.getName();

            objdescriber_list.add(object_meta_description);
        }

        return JSON.serialize(objdescriber_list);
    }

so that means we'll have something like this:

alt

Next step, is retrieve all fields related to that object, then I have to invoke other function:

$( "#options" ).change(function() {
    $("#options_fields").empty();
	$("#option_relationship").empty();
    schema();				
});

function schema(){

				Visualforce.remoting.Manager.invokeAction(
			        '{!$RemoteAction.myclass.getSchemaFields}',$( "#options option:selected" ).text(),
			        function(result, event){
			            if (event.status) {			            	
			            	var data = result.replace(/(&quot\;)/g,"\"");
			            	//console.log('data'+data);
			            	
			            	var information = jQuery.parseJSON(data);
			            	var options = $("#options_fields");
			            	options.append($("<option />").val('Selecciona').text('Selecciona'));
			            	$.each(information, function (index, value) {
			            		//console.log('|DEBUG|: '+value.label +'-'+value.name); 
			            		options.append($("<option />").val(value.key).text(value.value));
			            		$.unblockUI();

							});
			               	            	
            
			            } 
			            else if (event.type === 'exception') {
			               console.log('exception'+event.message);
			            } 
			            else {
			               console.log('other error'+event.message);
			            }
			        }, 
			        {escape: true}
			    );
			}

and the apex remote function:

@RemoteAction
	public Static String getSchemaFields(String object_selected){

		String selectedObject                     = object_selected;
		Map<String, Schema.SObjectType> schemaMap = Schema.getGlobalDescribe();
		Schema.SObjectType ObjectSchema           = schemaMap.get(selectedObject);
		Map<String, Schema.SObjectField> fieldMap = ObjectSchema.getDescribe().fields.getMap();
		List<FieldMap> fieldNames                   = new List<FieldMap>();

	    for(String fieldName: fieldMap.keySet()) {  
	        
	        FieldMap fmap = new FieldMap();
	        fmap.key = fieldMap.get(fieldName).getDescribe().getName();
	        fmap.value = fieldMap.get(fieldName).getDescribe().getLabel();
	        fmap.relationship = false;
	        
	        fieldNames.add(fmap);
	        
	        
        	if(fieldMap.get(fieldName).getDescribe().getType() == Schema.DisplayType.REFERENCE){
        		FieldMap fmap2 = new FieldMap();
        		fmap2.key = 'mssgrelationship_'+fieldMap.get(fieldName).getDescribe().getReferenceTo().get(0)+'_'+fieldMap.get(fieldName).getDescribe().getName();
        		String valueScaped = fieldMap.get(fieldName).getDescribe().getLabel().replace('ID','')+'***';
	        	fmap2.value = valueScaped.unescapeHtml4();
        		fmap.relationship = true;
        		fieldNames.add(fmap2);
        	}	           
	    }

		//fieldNames.sort();
		return JSON.serialize(fieldNames);
	}

So until here we have the main object and the fields we could select

alt

The next step is every time I select a field I want to put it in the text area so for that I use this:


$( "#options_fields" ).change(function() {
				
				$("#option_relationship").empty();
				
				var objectFields = $("#options_fields option:selected").val();


				if(objectFields.indexOf('mssgrelationship') !=-1){
					var splited_value = objectFields.split("_");					
					relationshipObject = splited_value[1];
	            	schema_relationship(splited_value[1]);
				}
				else{
					var caretPos = document.getElementById("textareaid").selectionStart;
				    var textAreaTxt = jQuery("#textareaid").val();
				    var txtToAdd = "{"+$("#options option:selected").val().toLowerCase()+"."+objectFields+"}";
				    jQuery("#textareaid").val(textAreaTxt.substring(0, caretPos) + txtToAdd + textAreaTxt.substring(caretPos) );					
				}
				
			});

So every time I select a field, I'll see it in my text area like this:

alt

then the funny part is to save my template:

$( "#btnsave" ).click(function() {
				readText($('#textareaid').val());
			});


			function readText(textoeditable){				

				Visualforce.remoting.Manager.invokeAction(
		            '{!$RemoteAction.myclass.generateQuery}',textoeditable,$("#options option:selected").text(),$('#template_name').text(), 
		            function(result, event){
		                if (event.status) {
		                	var response = result.replace(/(&quot\;)/g,"\"");	                
		                	var array = JSON.parse(response);
		                	//console.log(array);
		                	$.each( array, function( key, value ) {
		                		//console.log(value);
		                		list_fields.push(value);
		                	});
		                } 
		                else if (event.type === 'exception') {
		                	console.log(event.type);
		                } 
		                else {
		                    console.log(event.message);
		                }
		            }, 
		            {escape: true}
		        );
			
			}

and my long and probably not the best class:

@RemoteAction
	public static String generateQuery(String messageText,String objectSelected,String templateName){

		Map<String,String> map_parameterValue = new Map<String,String>();

		List<String> ls = new List<String>();
		string subject = messageText;
		Pattern p = Pattern.compile('\\{(.*?)\\}');
		Matcher m = p.matcher(subject);
		
		//storing information
		while (m.find()){		
		   ls.add(m.group().replace('{','').replace('}',''));
		}

		Map<String,String> map_splitedField = new Map<String,String>();
		Map<String,FieldMap> map_splitedField2 = new Map<String,FieldMap>();


		TemplateStructure template_record = new TemplateStructure();
		template_record.mainObject =objectSelected ;
		List<FieldMap> fieldmap_record = new List<FieldMap>();


		for(String field:ls){	
			
			List<String> splitList = field.split('\\.');
			
			for(Integer i=1;i<splitList.size();i++){

				
				if(splitList.size()==3){
					FieldMap fm = new FieldMap();
					fm.key = '{'+field+'}';
					fm.value = splitList[1]+'.'+splitList[2];
					map_splitedField.put('{'+field+'}',splitList[1]+'.'+splitList[2]);
					map_splitedField2.put('{'+field+'}',fm);
				}
				else{
					FieldMap fm = new FieldMap();
					fm.key = '{'+field+'}';
					fm.value = splitList[i];
					map_splitedField.put('{'+field+'}',splitList[i]);
					map_splitedField2.put('{'+field+'}',fm);
				}				
			}
		}

		for(FieldMap field_map:map_splitedField2.values()){
			fieldmap_record.add(field_map);
		}
		
		//System.debug('fieldmap_record@:'+fieldmap_record);

		template_record.fields = fieldmap_record;

		String commaSepratedFields = '';
		
		for(FieldMap fieldName:template_record.fields){
			if(commaSepratedFields == null || commaSepratedFields == ''){
				 commaSepratedFields = fieldName.value;
			}
			else{
				commaSepratedFields = commaSepratedFields + ', ' + fieldName.value;
			}
		}

		String query = 'select ' + commaSepratedFields + ' from ' + template_record.mainObject + ' limit 1';

		//System.debug('query@:'+query);

		template_record.query = query;
		

		mssgdirect__Template__c sfdc_template_record = new mssgdirect__Template__c();
		sfdc_template_record.mssgdirect__JSON_Message_Text__c = JSON.serialize(template_record.fields);
		sfdc_template_record.mssgdirect__Message_Text__c = subject;
		sfdc_template_record.mssgdirect__Name__c = templateName;
		sfdc_template_record.mssgdirect__Query__c = template_record.query;
		sfdc_template_record.mssgdirect__SObject__c = template_record.mainObject;

		insert sfdc_template_record;

		return sfdc_template_record.Id;
	}

And the idea is to have something like this:

alt

wich contains a JSON object that allows me to catch information of the kay and value to retrieve that information, and also a pre-query builded (not really sure if it's useful)

So once we have this stored, the next step is to translate that JSON in a query.

For that I used something like this code (it's not the fully one but you can get the idea)

List<mssgdirect__Template__c> listTemplate = new List<mssgdirect__Template__c>();

		for(mssgdirect__Template__c templateInfo:[Select Id,mssgdirect__Query__c,mssgdirect__Message_Text__c,mssgdirect__SObject__c,mssgdirect__JSON_Message_Text__c  
							from mssgdirect__Template__c where Id IN:templateIdSet]){

			templateQuery = templateInfo.mssgdirect__Query__c;
			listTemplate.add(templateInfo);
			mainObject = templateInfo.mssgdirect__SObject__c;
		}

		SObjectType objectType                  = Schema.getGlobalDescribe().get(objectName);
		Map<String,Schema.SObjectField> mfields = objectType.getDescribe().fields.getMap();
		Map<String,String> mapFieldValue        = new Map<String,String>();
		List<String> fields                     = new List<String>(mfields.keySet());
		List<Object> list_keyvalues             = new List<Object>();
		String message                          = '';
		String cellphone = '';

		String query = 'Select '+' '+phone_number_field+' from '+objectFlow+' where Id=:recordId';

		System.debug('Query flow: '+query);
		
		for(SObject sob:Database.query(query)){
			cellphone = String.valueof(sob.get(phone_number_field));
		}

		System.debug('cellph'+cellphone.replace('(','').replace(')','').replace('-','').replace(' ',''));


		for (SObject sob : Database.query(''+ ' select ' + String.join(fields, ',')
									        + ' from ' + objectType
		       							+ ' where Id=:recordId limit 1')) {
			System.debug('sob: '+sob);

		    for (String field : fields) {
		        Object value = sob.get(field);
		        if (value != null){
		        	mapFieldValue.put(field, String.valueOf(value));
		        }
		    }
		}

		System.debug('mapFieldValue: '+mapFieldValue);

		

		for(mssgdirect__Template__c templateInfo:listTemplate){

			list_keyvalues = (List<Object>)JSON.deserializeUntyped(templateInfo.mssgdirect__JSON_Message_Text__c);
			message = templateInfo.mssgdirect__Message_Text__c;
		}
		
		System.debug(list_keyvalues);
		String othertext = '';
		String lastText  = '';
	

		for(Object object_keys:list_keyvalues){
			
			Map<String, Object> object_map = (Map<String, Object>)object_keys;
			String llave = String.valueof(object_map.get('key'));
			String value = String.valueof(object_map.get('value'));

			if(mapFieldValue.get(value.toLowerCase())!=null){
				message = message.replace(llave,mapFieldValue.get(value.toLowerCase()));
			}	
			else{
				message = message.replace(llave,' ');
			}	
		}	

The result is a payload with the substitution of all the fields (so far there're not relationship inside but actually you can implement it.)

Please let me know if you have question or maybe I can go a little bit deep on this is you guys need.

Hope this help!

Edgar Moran
Author

Edgar Moran

Software Engineer, passionate for tech stuff video and photography, Salesforce and Mulesoft developer

Comments