Saturday, April 12, 2014

Add and remove email recipients dynamically

Introduction


This is the second post in a series of 5 post revolving around sending an Opportunity Report as an email attachment to members of the Opportunity team on a weekly basis. Here is the first post: Building an email recipient list from pre-selected users

Problem Definition


In addition to selecting members of the opportunity team to get the weekly opportunity report, Sales Managers might want to send the email report to recipients who are not part of the sales team. They would like these extra recipients to be remembered  so they don't have to type them each week. They would also like to be able to remove them from the list if they should not get the report for  a particular week. 

Solution


If additional recipients exist, show each of them in a text box and next to the text box a "Remove Recipient" button. Also add a "Add Recipient" button. Clicking on the button should add a new text box - "Remove Recipient" button pair above it. Additional recipients will be stored on a custom field on the Opportunity object. For this example, I will use the same field mentioned in the post above.

Your end result could look like this;



Apex


In the Controller Extension we need the following;
- a wrapper class to encapsulate the additional recipients
- a property to bind our wrapper class list to our page
- a list of previously added additional recipients 
- functions to add and remove additional recipients

Wrapper Class


A wrapper class is a class that "wraps" or "encapsulates" the functionality of another or component with the intention of adding a layer of indirection and abstraction. 

 public class AddionalRecipients{  
      public String emailAddress { get; set; }  
      public Integer position { get; set; }  
      public String textClass { get; set; }  
      public AddionalRecipients(String email, Integer recCount, String txtClass){  
           position = recCount;  
           emailAddress = email;  
           textClass = txtClass;  
      }  
 }  



Our wrapper class has a property which binds an additional email address and wraps it together with it's position in the list. This will be needed when removing an email address from the list. For cosmetic reasons we also have a textClass property which will be used to differentiate between an actual email address and a default text used in the input text field. More on this later.

Property


 public List<AddionalRecipients> additionalRecipients { get; set;}  

Previously added recipients



This is a tricky one. Imagine that we have a field Email_Recipients__c on Opportunity which holds comma separated email addresses of selected opportunity team members (from the post mentioned in the Introduction) plus additional recipients added using the functionality we are building here. Trick question - how would you separate them?. Think about it at check out the function "buildListOfAdditionalRecipients" at the end of this section. 
After this point you should end up with a list, that holds email addresses of additional recipients from the previous week. Now wrap them using our wrapper class as follows;

 private void buildAdditionalRecipientsList(List<String> additionalEmails){  
      this.additionalRecipients = new List<AddionalRecipients>();  
      for(Integer i= 0; i < additionalEmails.size(); i++){  
           if(additionalEmails[i].equals('Type new recipient Email!')){  
                this.additionalRecipients.add(new AddionalRecipients(additionalEmails[i], i, 'defaultText'));       
           } else {  
                this.additionalRecipients.add(new AddionalRecipients(additionalEmails[i], i, 'realText'));  
           }  
      }  
 }  

Here we use the traditional "for" loop because we want to know the exact index of the email in our list. If the user decides to remove the a user from the list, this will ensure that we are removing the right user from the list.

"defaultText" is the class name which specifies the style applied to the default text which is "Type new recipient Email!". If we are dealing with an email address that was previously used we want to apply a different style "realText". 

There is a good reason for putting the wrapping of additional emails in a function of its own. Will explain in a minute. Read on.

Add recipient action


Clicking on the "Add Recipient" Button calls an action which looks like this;

 public void addRecipient(){  
      List<String> additionalEmails = new List<String>();  
      for(AddionalRecipients ar : this.additionalRecipients){  
           additionalEmails.add(ar.emailAddress);  
      }  
      additionalEmails.add('Type new recipient Email!');  
      this.buildAdditionalRecipientsList(additionalEmails);  
 }  

First we "unwrap" the list of additional recipients, add a new one with the default text and wrap them again by passing them to the "buildAdditionalRecipientsList" function. Pretty straight forward.

Remove recipient action


To remove an additional recipient from the list, the following action is called;

 public void removeRecipient(){  
      List<String> additionalEmails = new List<String>();  
      this.additionalRecipients.remove(rowToRemove);  
      for(AddionalRecipients ar : this.additionalRecipients){  
           additionalEmails.add(ar.emailAddress);  
      }  
      this.buildAdditionalRecipientsList(additionalEmails);  
 }  

A property "rowToRemove" is used to communicate the index of the entry to remove from the list. 
public Integer rowToRemove { get; set; } 

First the entry is removed, then the list of remaining emails is unwrap and wrapped again using the wrapper class. Trick question - so why is it necessary to unwrap? why not just leave the list as it is?. 
The reason is because we always want to have a clean fresh list every time we remove an entry from the list. Avoiding to do so will lead to an "Index Out of Bound Exception", when you try to remove an entry from the list, whose index does no longer exist because the List reduced in size when a recipient was previously removed. My clinical approach is therefore to always bind a fresh list each time a recipient is removed from the list. 

Finally going back to the question of how to separate, the team members email addresses from the additional recipients added through the "Add recipient" Button, a possible solution could look like this

 public void buildListOfAdditionalRecipients(){  
      List<String> emailAddress = new List<String>();  
      if(this.opportunity.Email_Recipients__c != null){  
           emailAddress = new List<String>(this.opportunity.Email_Recipients__c.split(','));  
      }  
      List<String> additionalEmails = new List<String>();  
      Map<String, String> teamMemberMap = new Map<String, String>();  
      for(TeamMembers tm : this.OpportunityTeam){  
           teamMemberMap.put(tm.teamMember.User.Email, tm.teamMember.User.Email);  
      }  
      for(String emailAdd : emailAddress){  
           if(!teamMemberMap.containsKey(emailAdd)){  
                additionalEmails.add(emailAdd);  
           }  
      }  
      if(additionalEmails.isEmpty()){  
           additionalEmails.add('Type new recipient Email!');  
      }  
      this.buildAdditionalRecipientsList(additionalEmails);  
 }  

Visualforce


In Visualforce we will loop through the additional recipients list, and build the input text fields and the remove button as follows:

 <apex:outputPanel id="additionalRecipients">  
      <apex:repeat value="{!additionalRecipients}" var="recipient">  
           <apex:inputText id="addedEmail" value="{!recipient['emailAddress']}" styleClass="additionalRecipients {!recipient['textClass']}" onfocus="removeDefaultText('{!$Component.addedEmail}');" />  
           <apex:commandButton value="Remove Recipient" action="{!removeRecipient}" rerender="additionalRecipients">  
           <apex:param name="rowToBeRemoved" value="{!recipient['position']}" assignTo="{!rowToRemove}" />  
           </apex:commandButton>  
           <br />  
      </apex:repeat>  
 </apex:outputPanel>  

Notice the "assignTo" attribute which assigns the index of the additional recipient to be removed to the "rowToRemove" property. Also note the "rerender" attribute. Having the Controller Extension bind a fresh list every  time an additional recipient is removed from the list works thanks to this attribute. This attributes refreshes the div element identified by "additionalRecipients" ensuring that what we see on the page is consistent with what is in the Controller Extension.

Also note the "removeDefaultText" javaScript function which removes the default text once we click into the text field.

 <script type="text/javascript">  
      function hasClass(element, cls){  
           return(' ' + element.className + ' ').indexOf(' ' + cls + ' ') > -1;  
      }  
      function removeDefaultText(idOfAddedField){  
           var element = document.getElementById(idOfAddedField);   
           if(element.value == 'Type new recipient Email!'){  
                element.value = '';  
                if(hasClass(element, 'defaultText')){  
                     element.className = element.className.replace(' defaultText', ' realText');  
                }  
           }  
      }  
 </script>  

Finally the "Add Recipient" Button could look like this;

 <apex:pageBlockSectionItem >  
      <apex:commandButton value="Add Recipient" action="{!addRecipient}" rerender="additionalRecipients" />  
 </apex:pageBlockSectionItem>  

Conclusion

I hope this post was helpful to you.One last trick quest though -  how would you handle issues that might arise from users trying to send emails with default text in the input text fields, or empty input text fields because they clicked in them and type nothing? Would you simply ignore these or would you notify the user to remove unnecessary fields etc.?. These and more will be part of the next Post.
Finally, please challenge the contents of this post, add value to it in the comments if you can, so that it can help others viewing it in the future. God bless

No comments:

Post a Comment