Tech blog to share information.

April 8, 2019

A curious case of Javascript .apply() and Salesforce ActionFunction.

April 08, 2019 Posted by tjcool , , No comments

A curious case of Javascript .apply() and Salesforce ActionFunction.

Personally, I always like working on javascript. More I explore js, more I learn. And yes it always excites me. This motivates me to learn continuously. On the Same front, I was exploring dynamic function calling in JS. In Salesforce, calling function dynamically is altogether a challenge(only way to do so, is using Type class). We all know we can invoke function dynamically in JS using various way like applybindcall. While exploring .apply() function, I started looking into possible use cases of a .apply(), as I always believe in learning with the use case. We will see, why I was exploring apply.
Who are new to JS or dynamic function invocation, let me give the quick overview. In JS, we can create functions, functions are building blocks that hold responsibility. The responsibility of performing operations, retrieving information and many more. Now, say you are working on a system like a rule engine and you need to evaluate whether condition matches or not and then you need to call some function. To do so, you can use either call, apply or bind.
Use .bind() when you want that function to later be called with a certain context, very useful in events. Whereas, .call() or .apply() when you want to invoke the function immediately, and modify the context. Call() invokes the function and allows you to pass in arguments one by one whereas Apply()invokes the function and allows you to pass in arguments as an array. Here passing parameters in form of array is key and this is a reason I was exploring .apply(). As I was talking about learning with the use case below use case I picked.

Salesforce ActionFunctions

In Salesforce, action functions are used to call controller methods in an async way. The usual flow would be
Create Action-function, Create JS function(that retrieve values,validations), Call JS function that will call Action function.
The main bottleneck(for me), in calling action function from JS functions is passing parameters and validations. Validations can be easily taken care of by JS library but for passing parameter its not the case. Also if the VF page is heavily driven by action function, then you ended up writing so many JS functions. This leads to code readability and maintainability issue.

How can we use Apply() to solve the above problem?

As I mentioned earlier, passing parameters in form of array is key. Why ? because for every action function number of parameters needed is going to be different. So passing it an array is a good option.
Step 1 — Configurations
Now let see, some configuration part, here I have used Html-5 data attributes to hold configuration. If you want to hold any custom attributes, and want to access it pretty easily, you can go for data-* attribute. All you need to do is define data-<custom attribute name> at the element level. And how to access it, using property <element>.dataset.<attributename> . Let see how I used it. I created input buttons and at input buttons, I have created a custom attribute “data-actionfunction” where I am storing a name of action function to be called. While calling action function, we also need to pass parameters, so I have created one more attribute “data-parameter”. To uniquely identify, buttons having action function configured I have attribute “data-action”. Below is the snippet for same.
<input id=”btnAction1" type=”button” data-action=”register” data-actionfunction=”act1" data-parameter=”#txt1" value=”Action1"/>
Step 2 — Event Registration.
As you can see in the above example, I have not added any on-click event. So I will be doing that in this step. Now using attribute “data-action”, we will retrieve all buttons where action function is configured.
// retrieve all buttons
let domelements=_document.querySelectorAll(“input[data-action=’register’]”);
domElements.forEach(function(domElement,index){
    
    //get action function name using dataset. 
    let actionfunction=domElement.dataset.actionfunction;
    if(!!actionfunction){
       // register event.
       domElement.addEventListener(“click”,onclick,false);
    }
});
Step3 — Calling ActionFunction
Step 1 and 2 we are done with the initial setup of configuration and event registration. In this step, we will be doing, retrieve elements value and calling action function with parameters. To do, again dataset attribute will be used. Now, whenever the button is clicked, we will get that button instance using the keyword “this” and we will use it for attribute retrieval.
const scope=this;
let actFunction=scope.dataset.actionfunction;
let parameters=scope.dataset.parameter;
Now using parameter attribute we can do a query on an element and retrieve its value. Below is snippet for same.
function onclick(){
   const scope=this;
   //get action fucntion name
   let actFunction=scope.dataset.actionfunction;
   
   //get elements name, that values need to pass.
   let parameters=scope.dataset.parameter;
   let selectors=parameters.split(“,”);
   let values=[]; // values to be send, hold in this array.
   selectors.forEach(function(dom,index){
        var domEle=document.querySelector(dom);
        values.push(domEle.value);
   });
}
Now the key to call action function is a window variable. Its a global variable, representing the window in which the script is running, is exposed to JavaScript code. So to call action function below code is used.
window[actFunction].apply(null,values);
Complete Function Snippet-
function onclick(){
const scope=this;
//get action fucntion namelet actFunction=scope.dataset.actionfunction; 
//get elements name, that values need to pass.
let parameters=scope.dataset.parameter;
let selectors=parameters.split(“,”);
let values=[]; // values to be send, hold in this array
selectors.forEach(function(dom,index){
   var domEle=document.querySelector(dom);
   values.push(domEle.value);
});
  //as we don’t need to pass any context so first param is null.
  window[actFunction].apply(null,values);
}

Complete snippet

(function(_window,_document){
    let configuration={
       selector:"input[data-action='register']"
     }
     this.ActionWrapper=function(){
        console.log("Action Wrapper Initialized");
     }
     function onclick(){
       const scope=this;
       let actFunction=scope.dataset.actionfunction;
       let parameters=scope.dataset.parameter;         
       let selectors=parameters.split(",");
       let values=[];
       selectors.forEach(function(dom,index){
          var domEle=document.querySelector(dom);
          values.push(domEle.value);
       });                
       window[actFunction].apply(null,values);
     }
     
     this.ActionWrapper.prototype.config=configuration;
      
     this.ActionWrapper.prototype.Initialize=()=>{
       let eleSelector=configuration.selector;
       let eleCount=_document.querySelectorAll(eleSelector);
         SubscribeEvents(eleCount);
       }
     let SubscribeEvents=(domElements)=>{
         domElements.forEach(function(domElement,index){
            let actionfunction=domElement.dataset.actionfunction;
            if(!!actionfunction){
               domElement.addEventListener("click",onclick,false);
             }
          });
      }            
     _window.onload=function(){
           let wrapper=new ActionWrapper();
           wrapper.Initialize();
     }                
})(window,document);
//VF code 
<input id="btnAction1" type="button" data-action="register" data-actionfunction="act1" data-parameter="#txt1" value="Action1"/>
<!-- Action Function Starts-->
<apex:actionFunction name="act1" action="{!ActionCall}" reRender="dummy">
     <apex:param name="param1" assignTo="{!parameter1}" value=""/>
</apex:actionFunction>
I am working on a complete wrapper, where validation, screen loader will be taken care of from wrapper only.

If you have any comments or suggestion, please do let know. 


0 comments: