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"/> 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.