Tuesday, 12 April 2011

Edit Parent and Child Records with Visualforce - Part 2

In Part 1 of this topic, I covered a simple page that allowed editing of an account and its associated contacts.  At the end of that post I promised an improved version of the page and here it is.

The page markup is shown below:

<apex:page standardController="Account"
           extensions="AccountAndContactsEditExtension"
           tabStyle="Account" title="Prototype Account Edit">
    <apex:pageMessages />
    <apex:form >
        <apex:pageBlock mode="mainDetail">
            <apex:pageBlockButtons location="top">
                <apex:commandButton action="{!cancel}" value="Exit" />
                <apex:commandButton action="{!save}" value="Save" />
                <apex:commandButton action="{!newContact}" value="New Contact" rendered="{!NOT(ISBLANK(Account.id))}" onclick="showpopup(); return false;"/>
            </apex:pageBlockButtons>
            <apex:pageBlockSection title="Account Details" collapsible="true" id="mainRecord" columns="2" >          
            <apex:repeat value="{!$ObjectType.Account.FieldSets.AccountsAndContactsEdit}" var="field">
               <apex:inputField value="{!Account[field]}" />
            </apex:repeat>
            </apex:pageBlockSection>
           <apex:outputPanel id="contactList"> 
                <apex:repeat value="{!contacts}" var="contact" >
                    <apex:pageBlockSection columns="1"  title="Contact {!contact.Name}" collapsible="true">
                        <apex:pageBlockSectionItem >
                              <apex:pageBlockSection columns="2">
                 <apex:repeat value="{!$ObjectType.Contact.FieldSets.AccountsAndContactsEdit}" var="field">
                     <apex:inputField value="{!contact[field]}" />
                    </apex:repeat>
                              </apex:pageBlockSection>
                           </apex:pageBlockSectionItem>
                        </apex:pageBlockSection>
                       <div style="text-align:center">
                           <apex:commandButton value="Delete This Contact" onclick="idToDelete='{!contact.id}'; showpopup('deletecontent'); return false;"/>
                       </div>
                </apex:repeat>
            </apex:outputPanel>
            
       </apex:pageBlock>
  <apex:outputPanel id="addPanel">
  <apex:actionRegion id="popupRegion">
   <div id="opaque"/>
      <div id="popupcontent" class="popupcontent" style="width: 250px; height: 100px;">
     Please enter the new contact details<br/>
     <apex:outputLabel value="First Name: "/><apex:inputText id="newfirst" value="{!newContactFirstName}"/><br/>
    <apex:outputLabel value="Last Name: "/><apex:inputText id="newlast" value="{!newContactLastName}"/>
    <br/>
        <apex:commandButton id="cancelBtn" value="Cancel" onclick="hidepopup(); return false;"/>
       <apex:commandButton id="confirmBtn" action="{!newContact}" value="Create" rerender="contactList, addPanel" onclick="hidepopup();" status="working"/>
      </div>
   </apex:actionRegion>
   </apex:outputPanel>
  <apex:actionRegion id="deleteRegion">
      <div id="deletecontent" class="popupcontent" style="width: 250px; height: 100px;">
     Are you sure you wish to delete contact?
    <br/>
        <apex:commandButton id="cancelDelBtn" value="Cancel" onclick="hidepopup('deletecontent'); return false;"/>
       <apex:commandButton id="confirmDelBtn" value="Delete" rerender="contactList" onclick="hidepopup('deletecontent'); alert('Deleting contact ' + idToDelete); deleteContact(idToDelete); return false;" status="working"/>
      </div>
   </apex:actionRegion>
      <apex:actionFunction name="deleteContact" action="{!deleteContact}" rerender="contactList" status="working">
         <apex:param name="contactIdent" value="" assignTo="{!chosenContactId}"/>
      </apex:actionFunction>
    </apex:form>        
   <div id="workingcontent" class="popupcontent" style="width:150px; height:50px; margin-top:-100px; marginleft:-100px">
        <p align="center" style='{font-family:"Arial", Helvetica, sans-serif; font-size:20px;}'><apex:image value="/img/loading.gif"/>&nbsp;Please wait</p>
   </div>

  <apex:actionStatus id="working" onstart="showpopup('workingcontent');" onstop="hidepopup('workingcontent');" />
  <script>
   function showpopup(popupname)
   {
      var name="popupcontent";
      if (popupname)
      {
         name=popupname;
      }
      var popUp = document.getElementById(name);
      popUp.style.display = "block";
      document.getElementById('opaque').style.display='block';
   }
   
   function hidepopup(popupname)
   {
      var name="popupcontent";
      if (popupname)
      {
         name=popupname;
      }
      var popUp = document.getElementById(name);
      popUp.style.display = "none";
      document.getElementById('opaque').style.display='none';
   }
   
   var idToDelete;
   
  </script>
  <style>
  .popupcontent{
   position: fixed;
   top: 50%;
   left: 50%;
   margin-top: -100px;
   margin-left: -100px;
   display: none;
   overflow: auto;
   border:1px solid #CCC;
   background-color:white;
   border:3px solid #333;
   z-index:100;
   padding:5px;
   line-height:20px;
   font-size: 14px;
}
#opaque {
    position: fixed;
    top: 0px;
    left: 0px;
    width: 100%;
    height: 100%;
    z-index: 1;
    display: none;
    background-color: gray;
    filter: alpha(opacity=30);
    opacity: 0.3;
    -moz-opacity:0.3;
    -khtml-opacity:0.3
}
* html #opaque {
    position: absolute;
}
  </style>
</apex:page>

While it looks like the page is a good deal more complex, most of the additional markup is layered "popup" content or styling. Walking through the highlights we have:

<apex:commandButton action="{!newContact}" value="New Contact" rendered="{!NOT(ISBLANK(Account.id))}" onclick="showpopup(); return false;"/>

The New Contact command button now invokes some javascript to popup a layer. The user can enter the first and last name for the new contact and continue to create, or cancel out. The user's view is shown below- a definite improvement on the original which simply created a new contact called "Change Me".



Next up is the account detail - as threatened, this has been altered to use a field set rather than a hardcoded set of input fields - much more flexible:

<apex:repeat value="{!$ObjectType.Account.FieldSets.AccountsAndContactsEdit}" var="field">
     <apex:inputField value="{!Account[field]}" />
</apex:repeat>

Same goes for the contacts:

<apex:repeat value="{!$ObjectType.Contact.FieldSets.AccountsAndContactsEdit}" var="field">
 <apex:inputField value="{!contact[field]}" />
</apex:repeat>



Pressing the delete key caused an immediate delete of the contact - not so good if the user hit the button by mistake. Again, I've utilised javascript to popup a layer asking for confirmation.

<apex:commandButton value="Delete This Contact" onclick="idToDelete='{!contact.id}'; showpopup('deletecontent'); return false;"/>

Note that I've had to capture the id that has been chosen for deleting into a javascript variable - idToDelete. This used to be passed to the controller via an apex:param component on the commandbutton, but as the form isn't submitted until the user confirms, it has to be retained and passed as a parameter from the Delete button when the user confirms.

As seen by the user:



Finally, there are the popup layers. Note that each is contained in its own actionregion tag - this ensures that only the additional information captured in the layer is submitted back, rather than the account specific information etc.

<apex:outputPanel id="addPanel">
  <apex:actionRegion id="popupRegion">
   <div id="opaque"/>
      <div id="popupcontent" class="popupcontent" style="width: 250px; height: 100px;">
     Please enter the new contact details<br/>
     <apex:outputLabel value="First Name: "/><apex:inputText id="newfirst" value="{!newContactFirstName}"/><br/>
    <apex:outputLabel value="Last Name: "/><apex:inputText id="newlast" value="{!newContactLastName}"/>
    <br/>
        <apex:commandButton id="cancelBtn" value="Cancel" onclick="hidepopup(); return false;"/>
       <apex:commandButton id="confirmBtn" action="{!newContact}" value="Create" rerender="contactList, addPanel" onclick="hidepopup();" status="working"/>
      </div>
   </apex:actionRegion>
   </apex:outputPanel>

The controller hasn't changed an awful lot - there's just a couple of additional properties to capture the first and last name of the contact:

public class AccountAndContactsEditExtension {

    private ApexPages.StandardController std;
    
    // the associated contacts
   public List<Contact> contacts;
     
    // the chosen contact id - used when deleting a contact
    public Id chosenContactId {get; set;}
    
    public String newContactFirstName {get; set;}
    public String newContactLastName {get; set;}
    
    public AccountAndContactsEditExtension()
    {
    }
    
    public AccountAndContactsEditExtension(ApexPages.StandardController stdCtrl)
    {
     std=stdCtrl;
    }
    
    public Account getAccount()
    {
     return (Account) std.getRecord();
    }

 public SObject getSobject()
 {
  return std.getRecord();
 }
     
    private boolean updateContacts()
    {
        boolean result=true;
        if (null!=contacts)
           {
              // TODO: should work out what's changed and then save, easier to update everything for prototype
           List<Contact> updConts=new List<Contact>();
              try
              {
               update contacts;
              }
              catch (Exception e)
              {
                 String msg=e.getMessage();
                 integer pos;
                 
                 // if its field validation, this will be added to the messages by default
                 if (-1==(pos=msg.indexOf('FIELD_CUSTOM_VALIDATION_EXCEPTION, ')))
                 {
                    ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.ERROR, msg));
                 }
                 
                 result=false;
              }
           }
           
           return result;
    }
    
    public PageReference saveAndExit()
    {
     boolean result=true;
    result=updateContacts();
     
     if (result)
     {
        // call standard controller save
        return std.save();
     }
     else
     {
      return null;
     }
    }
    
    public PageReference save()
    {
     Boolean result=true;
     PageReference pr=Page.AccountAndContactsEdit;
     if (null!=getAccount().id)
     {
      result=updateContacts();
     }
     else
     {
      pr.setRedirect(true);
     }
     
     if (result)
     {
        // call standard controller save, but don't capture the return value which will redirect to view page
        std.save();
           ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.INFO, 'Changes saved'));
     }
        pr.getParameters().put('id', getAccount().id);
     
     return pr;
    }

    public void newContact()
    {
       if (updateContacts())
       {
          Contact cont=new Contact(FirstName=newContactFirstName, LastName=newContactLastName, AccountId=getAccount().id);
          insert cont;
        
          newContactFirstName=null;
          newContactLastName=null;
          contacts=null;
       }
    }
    
    public void deleteContact()
    {
       if (updateContacts())
       {
          if (null!=chosenContactId)
          {
             Contact cont=new Contact(Id=chosenContactId);
              delete cont;
       
              contacts=null;
              chosenContactId=null;
          }
       }
    }
    
   public List<Contact> getContacts()
    {
       if ( (null!=getAccount().id) && (contacts == null) )
       {
           contacts=[SELECT Id, Name, Email, Phone, AccountId, Title,  
                        Salutation, OtherStreet, OtherState, OtherPostalCode, 
                        OtherPhone, OtherCountry, OtherCity, MobilePhone, MailingStreet, MailingState, 
                        MailingPostalCode, MailingCountry, MailingCity, LeadSource, LastName, 
                        HomePhone, FirstName, Fax, Description, Department
                         FROM Contact 
                         WHERE AccountId = : getAccount().ID
                         ORDER BY CreatedDate];
       }
                          
       return contacts;
    }
}

and that's all there is to it - a real payoff in a much improved user experience with a small amount of development effort.

2 comments:

  1. Really helpful post!

    ReplyDelete
  2. great example but can you describe how to create apex:inputText dynamically (onclick)

    ReplyDelete