Custom Embedded JavaScript Functions: Difference between revisions
No edit summary |
No edit summary |
||
Line 33: | Line 33: | ||
* You must always return a string value – this return value will be your “transformation” of the [[Merge Fields and Tags|merge tag]]. | * You must always return a string value – this return value will be your “transformation” of the [[Merge Fields and Tags|merge tag]]. | ||
* You have access to "element", "documentData" and " | * You have access to "element", "documentData", "contextObject" and the function "callout" | ||
<syntaxhighlight lang="javascript"> | <syntaxhighlight lang="javascript" line="1"> | ||
//Wrapper element which will be inserted in the document at requested index | //Wrapper element which will be inserted in the document at requested index | ||
console.log('element: ', element); | console.log('element: ', element); | ||
Line 41: | Line 41: | ||
//The contextual record where the Function Expression is placed in | //The contextual record where the Function Expression is placed in | ||
console.log('contextObject: ', contextObject); | console.log('contextObject: ', contextObject); | ||
//A router that lets you call an apex class | |||
callout({ | |||
endp: 'classMethodName' | |||
},'ApexClassName').then(response => { | |||
console.log('callout response from function expression: '); | |||
console.log(response); | |||
}).catch(err => { | |||
console.error('error response from function expression: '); | |||
console.error(err); | |||
}) | |||
</syntaxhighlight>''Note: The "contextObject" is the document's "Core Object" record. If the function expression is embedded in a child repeatable context, then the contextObject becomes the child record.'' | </syntaxhighlight>''Note: The "contextObject" is the document's "Core Object" record. If the function expression is embedded in a child repeatable context, then the contextObject becomes the child record.'' | ||
== Callouts == | |||
Function Expressions can perform callouts to APEX via the callout() function that is passed to a function expression. This function is a wrapper around an authenticated AJAX call. Follow the instructions below to enable your function expressions to communicate with APEX. | |||
=== Apex === | |||
Firstly, you need to set up a new class that implements both b3d.GlobalRemotingInterface and Callable (native Salesforce class). | |||
Secondly, you need to implement a global method "getRemotingData()" which will get a JSON string of a JS Object with params. The important parameter is "endp" which will contain the name of the APEX method requested by the function expression.<syntaxhighlight lang="java" line="1"> | |||
global with sharing class ApexClassName implements b3d.GlobalRemotingInterface, Callable { | |||
//Required constructor | |||
global ApexClassName() { | |||
} | |||
//Required signature by b3d.GlobalRemotingInterface | |||
global String getRemotingData(String params) { | |||
try { | |||
Map<String, Object> requestObj = (Map<String, Object>)JSON.deserializeUntyped(params); | |||
return JSON.serialize(ApexClassName.call((String)requestObj.get('endp'), (Map<String, Object>)JSON.deserializeUntyped(params))); | |||
} catch(Exception e){ | |||
String generalError = String.format( | |||
Label.CRM_GeneralApexFailureRouter, | |||
new Object[]{ | |||
e.getMessage(), | |||
e.getLineNumber(), | |||
e.getStackTraceString() | |||
} | |||
); | |||
...error handling | |||
} | |||
} | |||
private static Object call(String action, Map<String, Object> args) { | |||
String recordId = (String)args.get('recordId'); //Template ID - always present | |||
switch on action { | |||
when 'classMethodName' { | |||
return classMethodName((String)args.get('param1')); | |||
} | |||
when else { | |||
return new b3d.ErrorResponse(String.format( | |||
Label.CRM_BadRemoteRequest, | |||
new Object[]{ | |||
ApexClassName.class.getName() | |||
} | |||
)); | |||
} | |||
} | |||
} | |||
protected static Object classMethodName(String param1){ | |||
//...Do something | |||
return new b3d.SuccessResponse([param1]); | |||
} | |||
} | |||
</syntaxhighlight>Please try to always return b3d.SuccessResponse or b3d.ErrorResponse within your APEX implementation. This will make it easier for our team to support your custom implementations in the future. | |||
=== Javascript === | |||
Now that you have the APEX class created successfully, you are ready to call it from within a function expression. Here is an example of how to call our sample class<syntaxhighlight lang="javascript"> | |||
callout({ | |||
endp: 'classMethodName' | |||
}, | |||
'ApexClassName').then(response => { | |||
console.log(response); | |||
}).catch(err => { | |||
console.error(err); | |||
}) | |||
</syntaxhighlight> | |||
== Examples == | == Examples == | ||
Line 48: | Line 127: | ||
=== Return Current Date === | === Return Current Date === | ||
This example is useful for getting a dynamically generated timestamp of the current date relative to when the document is opened in dd/mm/yyyy format (don’t worry, you can still apply [[Custom Value Formatting|custom value formatters]] and transform the date to a US date!)<syntaxhighlight lang="javascript" line="1"> | This example is useful for getting a dynamically generated timestamp of the current date relative to when the document is opened in dd/mm/yyyy format (don’t worry, you can still apply [[Custom Value Formatting|custom value formatters]] and transform the date to a US date!)<syntaxhighlight lang="javascript" line="1"> | ||
//Get current date in UK format | |||
() => new Date().toLocaleDateString(‘en-GB’); | () => new Date().toLocaleDateString(‘en-GB’); | ||
</syntaxhighlight><syntaxhighlight lang="javascript"> | |||
//Get current date, formatted based on user's browser locale | |||
() =>{ | |||
const currentDate = new Date(); | |||
return currentDate.toLocaleDateString(navigator.language); | |||
} | |||
</syntaxhighlight> | </syntaxhighlight> | ||
=== Embed Charts === | === Embed Charts === | ||
This example demonstrates how we can embed highcharts into a template. This example uses static data, but you can route with data from the template.<syntaxhighlight lang="javascript" line="1" start="1"> | This example demonstrates how we can embed highcharts into a template. This example uses static data, but you can route with data from the template.<syntaxhighlight lang="javascript" line="1" start="1"> | ||
console.log('element: ', element); | console.log('element: ', element); | ||
console.log('documentData: ', documentData); | console.log('documentData: ', documentData); | ||
console.log('contextObject: ', contextObject); | console.log('contextObject: ', contextObject); | ||
const reportData = {}; | |||
await callout({ | |||
endp: 'getReportData' | |||
},'FunctionExpressions').then(response => { | |||
console.log(response); | |||
reportData = response.responseObject; | |||
}).catch(err => { | |||
console.error(err); | |||
}); | |||
var script = document.createElement('script'); | var script = document.createElement('script'); | ||
script.src = 'https://code.highcharts.com/highcharts.js'; | script.src = 'https://code.highcharts.com/highcharts.js'; | ||
document.head.appendChild(script); | |||
let customId = Math.random().toString(36).slice(2, 7); | let customId = Math.random().toString(36).slice(2, 7); | ||
let chartWrapper = document.createElement('div'); | let chartWrapper = document.createElement('div'); | ||
Line 71: | Line 164: | ||
script.onload = function () { | script.onload = function () { | ||
window.addEventListener('documentLoaded', e => { | window.addEventListener('documentLoaded', e => { | ||
Highcharts.chart(customId, { | Highcharts.chart(customId, { | ||
title: { | title: { | ||
text: ' | text: 'Some Chart Header' | ||
}, | }, | ||
subtitle: { | subtitle: { | ||
text: ' | text: 'Example Chart Sub-Header' | ||
}, | }, | ||
yAxis: { | yAxis: { | ||
Line 86: | Line 178: | ||
xAxis: { | xAxis: { | ||
accessibility: { | accessibility: { | ||
rangeDescription: 'Range: 2010 to | rangeDescription: 'Range: 2010 to 2030' | ||
} | } | ||
}, | }, | ||
Line 102: | Line 194: | ||
} | } | ||
}, | }, | ||
series: | series: reportData.series, | ||
responsive: { | responsive: { | ||
rules: [{ | rules: [{ |
Revision as of 06:29, 6 October 2022
Intro
3B Docs allows you to write JavaScript code snippets that can be embedded on a document to extend existing functionality and/or create new functionality.
Usage
Most often, a Function Expression will be used to return strings like timestamps, user identifiable information (such as IP address, location, etc) and for locale, timezone and language reasons. Here are some other examples:
- To obtain a photo of the currently viewing user using their integrated camera and then embed it in the document
- To connect via REST API to an external service to fetch additional data for the contextual record
- To import an external library and display custom elements/components that are rendered by the external library (say a table, chart, image slider etc)
- To patch-fix a bug in 3B Docs
How To
In order to add custom JavaScript function, you need to create a Function Expression record, which you can access from the 3B Docs Application (or simply search for “Function Expressions” in the app launcher drop down).
When you have navigated to the object list view, you can click on the “New” button which will display the new record form. What is important here is that you give the Function Expression a unique name that has no spaces or special characters. Don’t worry about enforcing this rule yourself, we have built a salesforce validation rule to make sure that the name of the Value Formatter is compliant with 3B Docs.
To use the function expression in your template, navigate to the “Functions” icon within the Template builder.
You will see a drop-down of all the available Function Expressions that you can use within the Template.
When you select a specific Function Expression, it will be copied to your clipboard and you can simply paste it anywhere inside the Template document via CTRL + V / CMD + V (on Mac) command.
When you paste a function expression, it will look something like this: #{Functions.CurrentDate}
Function Expressions are treated just like merge fields, so you will have access to the custom value formatters that may be available to modify the value returned by the Function Expression. So, the following Function Expression is completely valid: #{Functions.CurrentDate> EU_Date_Format}
Syntax
Just like custom value formatters, when you create a Function Expression, you are basically creating a JavaScript function. This JavaScript function has access to the global context and thus to the user’s navigator object, the application variables & collections and much more.
The difference between custom value formatters and Function Expressions lies in the fact that custom value formatters can only be applied to a specific merge tag, whereas Function Expressions form their own independent merge tags.
Furthermore, here are a few considerations:
- You must always return a string value – this return value will be your “transformation” of the merge tag.
- You have access to "element", "documentData", "contextObject" and the function "callout"
//Wrapper element which will be inserted in the document at requested index
console.log('element: ', element);
//An object that is populated with all the data loaded from the server
console.log('documentData: ', documentData);
//The contextual record where the Function Expression is placed in
console.log('contextObject: ', contextObject);
//A router that lets you call an apex class
callout({
endp: 'classMethodName'
},'ApexClassName').then(response => {
console.log('callout response from function expression: ');
console.log(response);
}).catch(err => {
console.error('error response from function expression: ');
console.error(err);
})
Note: The "contextObject" is the document's "Core Object" record. If the function expression is embedded in a child repeatable context, then the contextObject becomes the child record.
Callouts
Function Expressions can perform callouts to APEX via the callout() function that is passed to a function expression. This function is a wrapper around an authenticated AJAX call. Follow the instructions below to enable your function expressions to communicate with APEX.
Apex
Firstly, you need to set up a new class that implements both b3d.GlobalRemotingInterface and Callable (native Salesforce class).
Secondly, you need to implement a global method "getRemotingData()" which will get a JSON string of a JS Object with params. The important parameter is "endp" which will contain the name of the APEX method requested by the function expression.
global with sharing class ApexClassName implements b3d.GlobalRemotingInterface, Callable {
//Required constructor
global ApexClassName() {
}
//Required signature by b3d.GlobalRemotingInterface
global String getRemotingData(String params) {
try {
Map<String, Object> requestObj = (Map<String, Object>)JSON.deserializeUntyped(params);
return JSON.serialize(ApexClassName.call((String)requestObj.get('endp'), (Map<String, Object>)JSON.deserializeUntyped(params)));
} catch(Exception e){
String generalError = String.format(
Label.CRM_GeneralApexFailureRouter,
new Object[]{
e.getMessage(),
e.getLineNumber(),
e.getStackTraceString()
}
);
...error handling
}
}
private static Object call(String action, Map<String, Object> args) {
String recordId = (String)args.get('recordId'); //Template ID - always present
switch on action {
when 'classMethodName' {
return classMethodName((String)args.get('param1'));
}
when else {
return new b3d.ErrorResponse(String.format(
Label.CRM_BadRemoteRequest,
new Object[]{
ApexClassName.class.getName()
}
));
}
}
}
protected static Object classMethodName(String param1){
//...Do something
return new b3d.SuccessResponse([param1]);
}
}
Please try to always return b3d.SuccessResponse or b3d.ErrorResponse within your APEX implementation. This will make it easier for our team to support your custom implementations in the future.
Javascript
Now that you have the APEX class created successfully, you are ready to call it from within a function expression. Here is an example of how to call our sample class
callout({
endp: 'classMethodName'
},
'ApexClassName').then(response => {
console.log(response);
}).catch(err => {
console.error(err);
})
Examples
Here are some examples to get you started with the basic syntax used by 3B Docs for Function Expressions:
Return Current Date
This example is useful for getting a dynamically generated timestamp of the current date relative to when the document is opened in dd/mm/yyyy format (don’t worry, you can still apply custom value formatters and transform the date to a US date!)
//Get current date in UK format
() => new Date().toLocaleDateString(‘en-GB’);
//Get current date, formatted based on user's browser locale
() =>{
const currentDate = new Date();
return currentDate.toLocaleDateString(navigator.language);
}
Embed Charts
This example demonstrates how we can embed highcharts into a template. This example uses static data, but you can route with data from the template.
console.log('element: ', element);
console.log('documentData: ', documentData);
console.log('contextObject: ', contextObject);
const reportData = {};
await callout({
endp: 'getReportData'
},'FunctionExpressions').then(response => {
console.log(response);
reportData = response.responseObject;
}).catch(err => {
console.error(err);
});
var script = document.createElement('script');
script.src = 'https://code.highcharts.com/highcharts.js';
document.head.appendChild(script);
let customId = Math.random().toString(36).slice(2, 7);
let chartWrapper = document.createElement('div');
chartWrapper.id = customId;
element.after(chartWrapper);
script.onload = function () {
window.addEventListener('documentLoaded', e => {
Highcharts.chart(customId, {
title: {
text: 'Some Chart Header'
},
subtitle: {
text: 'Example Chart Sub-Header'
},
yAxis: {
title: {
text: 'Number of Employees'
}
},
xAxis: {
accessibility: {
rangeDescription: 'Range: 2010 to 2030'
}
},
legend: {
layout: 'vertical',
align: 'right',
verticalAlign: 'middle'
},
plotOptions: {
series: {
label: {
connectorAllowed: false
},
pointStart: 2010
}
},
series: reportData.series,
responsive: {
rules: [{
condition: {
maxWidth: 500
},
chartOptions: {
legend: {
layout: 'horizontal',
align: 'center',
verticalAlign: 'bottom'
}
}
}]
}
});
})
};
return '';
Tips & Tricks
Here are some tips and tricks for using Function Expressions:
Block Document Renders in Certain Locales
You can throw a JavaScript error if you deem the currently viewing user’s locale as one that you do not support/allow.
Get Access to the Viewing User’s Hardware
You can request access to hardware such as GPS, Camera, Accelerometer, Barometer etc. and log the return inside the document.
API Callouts
You can use Function Expressions to do callouts, fetch data externally, load additional resources, use JavaScript Libraries such as FullCalendar, Chart.js, Datatables and many more..
Here is a sample of how to initialize FullCalendar (you may need extra code in order to import JS and CSS):
document.addEventListener('DOMContentLoaded', function() {
var calendarEl = document.getElementById('calendar');
var calendar = new FullCalendar.Calendar(calendarEl, {
initialView: 'dayGridMonth'
});
calendar.render();
});
User Logging, Tracking and Fingerprinting
You can use such technologies as Fingerprint to user tracking capabilities to your documents