Beware Bad Markup, When You Least Expect It!
Yesterday I deployed a very basic Visualforce page for a client, which I'd tested and worked fine in the sandbox. Once I'd finished deploying it I did the thing to do and I tested it again, only this time, the results were not so pretty: Something, somehow, was causing the re-rendered <apex:outputPanel>
to appear twice on the page, but only after I clicked an action button a second time. What's more, the reason I hadn't seen this originally is that I'd tested using Firefox in the sandbox, and then Chrome in production—on whim I tried another WebKit based browser, Safari, and found the issue occurred there as well. Around this point it was pushing on for 7pm, and with nothing but three short words running through my head (the first started with our friend "W" for "WHAT") I decided that it was best left until morning, especially since the page wasn't yet available to anybody.
This morning I registered a minty-fresh developer org, and set about reproducing the issue. Simply recreating the functionality didn't seem to do the trick, so I copied the Visualforce markup from the original page and then proceeded to strip it back until I had something very simple that still showed the problem, the entire page content is below.
<apex:page controller="LaceyController" showHeader="false" sidebar="false" cache="false" standardStylesheets="false">
<apex:form >
<apex:outputPanel id="thePanel">
<p><apex:messages/></p>
<apex:outputLabel for="theCheckBox" value="Toggle this...." />
<apex:inputCheckBox id="theCheckBox" selected="false" value="{!barryTheBool}" />
<apex:commandLink styleClass="button clear" action="{!FireInTheHole}" rerender="thePanel" value=" FIRE!"/>
</apex:outputPanel>
</apex:form>
</apex:page>
For the sake of completeness, this is the associated controller:
public with sharing class LaceyController
{
public boolean barryTheBool {get; set;}
public LaceyController()
{
barryTheBool = false;
}
public Pagereference FireInTheHole()
{
ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.INFO, 'Barry the bool is now ' + barryTheBool));
return null;
}
}
And this series of screenshots show what the various stages of the 'bug' look like:
Fig 1. The initial state of the page
Fig 2. The message displayed having checked the box and clicked the 'FIRE!' link
Fig 3. The view after clicking the 'FIRE!' link a second time
Fig 4. The page after unchecking the second checkbox, and clicking the second 'FIRE!' link
As you can see, the behaviour is rather bizarre to say the least. The reason I've disabled the standard stylesheets is because the page is to be exposed via sites, and if you want to see this in action it's available right here. Through a quick trial and error process and making use of the earlier clue (i.e. this only happened on the second and subsequent re-renders) I deduced that removing the <p></p>
tags from around the <apex:pageMessages/
> tag resolved the issue. Huh? Similarly, swapping <apex:pageMessages/>
for <apex:messages/>
also fixed the problem (in this case, since styling is not required the latter is the one to use, but I didn't author the original page). Since the markup was clearly the cause of the problem, I took the generated source for the page and pasted into w3.org's handy validator which flagged a few errors and warnings, one of which (the seemingly important one in this scenario) is below.
Fig 5. Some of the output from w3.org's validator
A block-level element, the <p></p>
tags in this case, can not be contained in an inline element, and guess what? The <apex:outputPanel>
by default is implemented via <span>
tags, and a <span>
is an inline element—mystery solved! Well, no, not really.
The easy fixes here are to remove the paragraph element, specify layout="block"
inside the <apex:outputPanel>
opening tag, or as mentioned, just replace <apex:pageMessages>
with <apex:messages>
. The mystery still remains because the last option still has the paragraph tags, and they're still contained within the <span>
from the <apex:outputPanel>
, so there's clearly something else at play here. However, the page now works and that's my immediate concern, so it's time to hang up my investigating hat for the day and carry on writing code.
Update (17/12/2011)
An anonymous commenter has filled in the rest of the details, for the sake of those viewing this article on the main page (i.e. with comments hidden) I'm adding it here for completeness. Thank you to whoever you are!
To answer you question, and are two different implementations: the former renders
<apex:messages/>
as ul/li elements and the latter a table of messages. In fact<apex:pageMessages/>
's table is wrapped in a div. A div, when used inside a p tag, will actually close the p tag. So, what's happening here is that when the re-rendered response is applied to the DOM, the p tag is closed and everything after the message div is moved outside of the "thePanel" span. Subsequent re-render responses only replace the "thePanel" element which, again, doesn't contain the label and input element and the elements are applied to as mentioned. The label and input elements are re-added, not replaced.