Introduction
The S-Docs Callable Apex feature allows you to generate merge field or table values in your S-Docs templates via custom Apex code. This feature is useful if your business requirements necessitate the use of advanced logic that can't be defined directly within an S-Docs template.
You can include as many callable apex classes in your template as you'd like, but we recommend writing a single class with multiple functions or arguments, as opposed to multiple classes.
Apex Class
Your Apex class must be global and return a <string, string> map where keys are represented by field names and values are represented by field values. The following example provides a basic overview of an acceptable class, however you can use multiple functions or arguments in your class.
global class CallableApexTest implements Callable { public Object call(String action, Map<String,Object> args) { switch on action { when 'getMergeFieldMapExample1' { return this.getMergeFieldMapExample1((String)args.get('recordId')); } when else { throw new ExtensionMalformedCallException('Method not implemented'); } } } public class ExtensionMalformedCallException extends Exception {} public Map<String,String> getMergeFieldMapExample1(String recordId) { Opportunity opp = [SELECT Id FROM Opportunity WHERE Id=:recordId]; // Base Object Record Map<String,String> mergeFieldMapExample = new Map<String,String>{ 'Field_1' => 'Value_1', 'Field_2' => 'Value_2', 'Field_3' => '<strong>Value_3</strong>' }; return mergeFieldMapExample; } }
Callable Syntax
To call your Apex class in your S-Docs template, navigate to the Source editor and use syntax similar to the following example:
<!--{{! <callable> <class>CallableApexTest</class> <action>getMergeFieldMapExample1</action> <args>{ "recordId" : "{{!Opportunity.Id}}", "SomeField" : "{{!Opportunity.Field__c}}" }</args> </callable> }}--> Field_1: {{!Field_1}}<br /> Field_2: {{!Field_2}}<br /> Field_3: {{{!Field_3}}}<br />
Lines [1-2] open the callable Apex statement. The Apex class name is referenced in <class> tags on line [3]. The function of the Apex class is referenced in <action> tags on line [4]. Arguments can optionally be referenced within <args> tags, as shown on line [5]. Finally, the statement is closed on lines [6-7].
You can include any merge fields referenced in your <string, string> map anywhere in your S-Docs template. The three fields from the example map are included on lines [8-10].
The example above would render the following document:
Example Use Case
To further understand this feature, let's take a look at an example use case.
Let's say that the Opportunity object contains a custom multi-select picklist field with 6 different products.
We'd like our document to display a list of 6 sections that output a letter that corresponds to the product's positioning in the chosen list. In other words, if all products are chosen, the S-Docs document should render like this:
But if Products 1, 3, and 6 are chosen, the S-Docs document should render like this:
To easily accomplish this, we can write an Apex class and then call that class in our S-Docs template. The Apex class for this function looks like this (and includes comments for clarification):
global class SDocsCallable implements Callable { public Object call(String action, Map<String,Object> args) { switch on action { when 'getSectionMergeFieldMap' { return this.getSectionMergeFieldMap((String)args.get('recordId')); } when else { throw new ExtensionMalformedCallException('Method not implemented'); } } } public class ExtensionMalformedCallException extends Exception {} /* Example input: Opportunity with a Products__c value of: Product 1;Product 3;Product6 To be clear: 'Product 1' // Included 'Product 2' // NOT Included 'Product 3' // Included 'Product 4' // NOT Included 'Product 5' // NOT Included 'Product 6' // Included Example output: { "Section 1" : "A", "Section 2" : "", "Section 3" : "B", "Section 4" : "", "Section 5" : "", "Section 6" : "C" } (these are mapped to merge fields in the S-Docs Template like so: {{!Section_1}} ) */ public Map<String,String> getSectionMergeFieldMap(String recordId) { String Products = [SELECT Products__c FROM Opportunity WHERE Id=:recordId].Products__c; List<String> productNames = new List<String>{ 'Product 1', 'Product 2', 'Product 3', 'Product 4', 'Product 5', 'Product 6' }; Map<String,String> mergeFieldMap = new Map<String,String>(); Integer currentChar = 65; for (Integer i = 0; i < productNames.size(); i++) { String mergeFieldValue = ''; if (Products.contains(productNames[i])) { mergeFieldValue = 'Section ' + String.fromCharArray( new List<Integer> { currentChar } ); currentChar++; } mergeFieldMap.put('Section_' + (i + 1), mergeFieldValue); } return mergeFieldMap; } }
Our S-Docs template Source code for this use case looks like this:
<!--{{! <callable> <class>SDocsCallable</class> <action>getSectionMergeFieldMap</action> <args>{ "recordId" : "{{!Opportunity.Id}}" }</args> </callable> }}-->Section 1: {{!Section_1}}<br /> Section 2: {{!Section_2}}<br /> Section 3: {{!Section_3}}<br /> Section 4: {{!Section_4}}<br /> Section 5: {{!Section_5}}<br /> Section 6: {{!Section_6}}<br />
When we generate the document, the correct letters correspond to the correct sections:
Callable Apex - Line Items
You can also call apex classes to generate table values in your S-Docs templates - this is done by placing your callable statement within a <lineitems> statement and including one of three return types in <returntype> tags. The three return types are SDocTable, ListSObject, and XML.
SDocTable
The SDocTable return type allows you to generate highly customized tables using logic otherwise not available within S-Docs templates. In an S-Docs template, the callable syntax for the SDocTable return type looks similar to the following:
<table><!--{{!<LineItems> <callable> <class>CallableApexTestlineitems</class> <action>getLineItemsAsSDocTable</action> <args>{ "recordId" : "{{!ObjectID18}}" }</args> <returntype>SDocTable</returntype> </callable> <class>table123</class> <column>product_rate_category</column> <column>domestic_rate_adjustment_1</column> <column>domestic_rate_adjustment_2</column> <column>domestic_rate_adjustment_3</column> </LineItems>}}--> </table>
The callable syntax is nested inside of a <LineItems> statement. Note the addition of <returntype> tags in line 6, which specify that the apex class will build and return an SDocTable. After the <callable> tags are closed out, the <LineItems> statement is written as normal.
The corresponding Apex class might look similar to the following:
global class CallableApexTestlineitems implements Callable { public Object call(String action, Map<String,Object> args) { switch on action { when 'getLineItemsAsSDocTable' { return this.getLineItemsAsSDocTable((String)args.get('recordId')); } when else { throw new ExtensionMalformedCallException('Method not implemented'); } } } public class ExtensionMalformedCallException extends Exception {} public Map<String,String> getScheduleMergeFieldMap(String recordId) { return new Map<String,String>{ 'Schedule_1' => 'Schedule A', 'Schedule_3' => 'Schedule B'}; } public SDOC.SDUtil.SDocTable getLineItemsAsSDocTable(String recordId) { Map<String,List<String>> rateCategoryMap = new Map<String,List<String>>{ 'Category C' => new List<String>{ 'C1', 'C2', 'C3' }, 'Category A' => new List<String>{ 'A1', 'A2', 'A3' }, 'Category B' => new List<String>{ 'B1', 'B2', 'B3' } }; List<String> rateCategoryKeysSorted = new List<String>{ 'Category A', 'Category B', 'Category C' }; SDOC.SDUtil.SDocTable table = new SDOC.SDUtil.SDocTable(); for (String rateCategory : rateCategoryKeysSorted) { SDOC.SDUtil.SDocTableRow row = new SDOC.SDUtil.SDocTableRow(); row.cells.add(new SDOC.SDUtil.SDocTableCell('product_rate_category', rateCategory)); Integer draNum = 1; for (String domesticRateAdj : rateCategoryMap.get(rateCategory)) { row.cells.add(new SDOC.SDUtil.SDocTableCell('domestic_rate_adjustment_' + draNum, domesticRateAdj)); draNum++; } table.rows.add(row); } return table; } }
The output document would look similar to this:
ListSObject
The ListSObject return type allows you to return a list of records similar to a <LineItemsSOQL> statement. Unlike <LineItemsSOQL> statements, however, you're given much more flexibility since the query is written in an Apex class. For example, you could filter records based on rich-text field data, which isn't normally possible. In an S-Docs template, the callable syntax for the ListSObject return type looks similar to the following:
<table><!--{{!<LineItems> <callable> <class>CallableApexTestlineitems</class> <action>getLineItemsAsListSObject</action> <args>{ "recordId" : "{{!Opportunity.Id}}" }</args> <returntype>ListSObject</returntype> </callable> <class>table499</class> <column>Name</column> <column>StageName</column> <column>Account.Name</column> <column>Account.Owner.Name</column> </LineItems>}}--> </table>
The corresponding Apex class might look similar to the following:
global class CallableApexTestlineitems implements Callable { public Object call(String action, Map<String,Object> args) { switch on action { when 'getLineItemsAsListSObject' { return this.getLineItemsAsListSObject((String)args.get('recordId')); } when else { throw new ExtensionMalformedCallException('Method not implemented'); } } } public class ExtensionMalformedCallException extends Exception {} public List<SObject> getLineItemsAsListSObject(String recordId) { return [SELECT Name, StageName, Account.Name, Account.Owner.Name FROM Opportunity WHERE AccountId != null LIMIT 3]; } }
The output document would look similar to this:
XML
The XML return type allows you to merge XML data into a table in your document. The XML can be hardcoded in your apex class or pulled from an external system, making this option great for merging external data into your templates. In an S-Docs template, the callable syntax for the XML return type looks similar to the following:
<table><!--{{!<LineItems> <callable> <class>CallableApexTestlineitems</class> <action>getLineItemsAsXMLString</action> <args>{ "recordId" : "Opportunity.Id" }</args> <returntype>XML</returntype> </callable> <class>table499</class> <listname>products</listname> <column>productname</column> <column>productdescription</column> <column>productcode</column> </LineItems>}}--> </table>
Note the addition of <listname> tags before the table <column> tags, which should be the same as the parent tags in your XML string. The corresponding Apex class might look similar to the following:
global class CallableApexTestlineitems implements Callable { public Object call(String action, Map<String,Object> args) { switch on action { when 'getLineItemsAsXMLString' { return this.getLineItemsAsXMLString((String)args.get('recordId')); } when else { throw new ExtensionMalformedCallException('Method not implemented'); } } } public class ExtensionMalformedCallException extends Exception {} public String getLineItemsAsXMLString(String recordId) { return '' + '<products>' + ' <productname>Apple</productname>' + ' <productdescription>Red fruit</productdescription>' + ' <productcode>4016</productcode>' + '</products>' + '<products>' + ' <productname>Banana</productname>' + ' <productdescription>Yellow fruit</productdescription>' + ' <productcode>4011</productcode>' + '</products>' + '<products>' + ' <productname>Pear</productname>' + ' <productdescription>Green fruit</productdescription>' + ' <productcode>3012</productcode>' + '</products>'; } }
The output document would look similar to this:
As you can see, the callable Apex feature allows you to perform complex functions in your documents that otherwise aren't available in the S-Docs template editor.