[jsr-314-open] JavaScript disabled support [Was: Outcome of JSFDays discussions]
Andy Schwartz
andy.schwartz at ORACLE.COM
Thu Apr 2 14:53:48 EDT 2009
Hey Martin -
Thanks for writing this up. (I decided to break the disabled JavaScript
topic out into a separate email thread to make this easier to track.)
Martin Marinschek wrote On 4/2/2009 3:24 AM ET:
>
> - In the presentation about JSF 2.0 features, there was a question
> from the audience what happens with AJAX support when JavaScript is
> disabled.
There are at least 3 different cases to consider:
1. Command components firing Ajax-based action events, eg:
<h:commandButton>
<f:ajax/>
</h:commandButton>
2. Editable value holders firing Ajax-based valueChange events, eg:
<h:inputText>
<f:ajax/>
</h:inputText>
3. Ajax-based events that other than #1 and #2, eg:
<h:panelGroup>
<f:ajax event="mouseOver"/>
</h:panelGroup>
Or even:
<h:commandButton>
<f:ajax event="mouseOver"/>
</h:commandButton>
Case #3 is the simplest, so let's look at it first. What is the correct
fallback behavior for client behaviors/scripts that are invoked in
response to user interface events such mouseOver/mouseOut, or say,
focus/blur? I don't believe that there is any meaningful fallback
content that we can provide here - a "Refresh" button isn't
appropriate. My feeling is that for these cases, the correct fallback
behavior is to do nothing. Application developers who need to build
applications that run with JavaScript disabled should only leverage such
events for progressive enhancement purposes - ie. such applications
should not rely on these user interface events as the only means to
access mission critical functionality. So, think we are okay for this
particular case - I don't see the need to do anything here.
Case #1 is more interesting... So what fallback behavior do we want when
we are using Ajax to send an action event to the server in response to a
click on a command component? The correct answer here is clear: when
JavaScript is disabled, the command component should still deliver the
action event, but via traditional form submission instead of over Ajax.
Actually, this is exactly the fallback behavior that we have in the
trivial <h:commandButton> example shown above... When Ajax is disabled,
the <h:commandButton> will still submit - it just an HTML <input
type="submit"> after all - and any registered action listeners will be
called. So we've got a little graceful degradation happening already.
Well, in this trivial case at least. However, there are at least two
cases where we do not have a solution:
- <h:commandButton> with <f:param>
- <h:commandLink> (with our without <f:param>)
Both of these cases currently require JavaScript execution for form
submission purposes. Note that this is not in any way specific to Ajax
or Behaviors, or even JSF 2.0. So we've got a more fundamental problem
here.
Leaving Ajax/Behaviors aside for a moment - how would we go about
solving more basic cases such as supporting <h:commandLink> when
JavaScript is disabled? The solution requires two pieces:
- We need some way to detect when JavaScript is disabled.
- Components that care need to render alternate content when JavaScript
is disabled.
For example, if we had a way for the <h:commandLink>'s renderer to
determine that JavaScript is disabled, then the renderer could fall back
to rendering an HTML <input type="submit"> instead of a link with an
onclick handler.
The <f:param> case is a bit trickier. I suppose that the link/button
renderer could spit parameter values out as hidden fields instead of
using JavaScript to include these values in the form dynamically. The
drawback of course is that the hidden fields will be submitted with
every postback, not just when the corresponding button is clicked. Or
perhaps we could use an HTML <button> element and encode the parameters
into the button's value attribute? I believe that Spring Faces has
solved this problem (Jeremy - can you comment)? Does cs-JSF deal with
this? Any other frameworks provide solutions for this?
In any case, the point is that for the command/action case, the solution
really lies in the component renderer - this is not an Ajax/Behavior
issue. If we solve the generic problem of supporting <h:commandLink>
and <h:commandButton> with <f:param> when JavaScript is disabled, then
this is sufficient to address the Ajax case as well.
Case #3 is the trickiest... There are actually two sub-cases here:
3A. The Ajax request is optional - Ajax is used to progressively enhance
the editable value component.
3B. The Ajax request is required - Ajax is used to provide functionality
that the end user needs in order to complete some task in the application.
An example of 3A might be Ajax-based validation. The application should
of course re-validate any data when the form is submitted. Disabling
JavaScript is not a problem here, since the Ajax-based functionality is
an enhancement, not a required feature.
The main use case for 3B is master-detail components. A common case is
using a select component as the master and updating the detail contents
based on the user's selection. For example, the user selects a car
manufacturer from an Ajax-enabled <h:selectOneMenu>. This populates a
second <h:selectOneMenu> with the appropriately filtered set of car
models. If JavaScript is disabled, and the <f:ajax event="valueChange">
is the only means provided by the application to trigger the population
of the second select component, any users who have disabled JavaScript
will be stuck.
There are a range of possible solutions here:
i. Give Behaviors the ability to render arbitrary contents
before/after/during component rendering.
One concern that I have about this approach is that it means that the
Behavior implementation needs to be familiar with the component - and
possibly the component's content - to know when to render, or whether to
render anything at all. For example, if the Behavior is attached to an
<h:selectOneMenu>, then it might want to render a "Refresh" button.
However, if the Behavior is attached to an <h:commandButton> (or some
other component, like an <h:panelGrid>), then no "Refresh" button should
be rendered.
I would prefer that we come up with a solution where the component
Renderer decides when/where to have the Behavior render its fallback
content, since the component Renderer knows both whether it requires
fallback content at all and also knows where the best placement of the
fallback content might be.
ii. Provide a more targeted Behavior API that component renderers can
use to ask a Behavior to render fallback content.
Instead of allowing Behaviors to render arbitrary content
before/after/during component rendering, we would instead provide a
couple of new methods, perhaps on ClientBehavior (not sure about the
method names):
- public boolean isFallbackAvailable()
- public void renderFallback(BehaviorContext)
Or, alternatively, we can add these on a new FallbackClientBehavior
interface (extends ClientBehavior).
Components/renderers that want to render fallback content (eg.
input/select components) would call renderFallback() at the appropriate
point in order to insert the Behavior's fallback content into the
response. Components/renderers that do not want to render fallback
content (eg. pretty much everything other than the input/select
components) wouldn't bother calling this.
iii. Let the component renderers provide fallback content.
The idea here is that the input/select component renderers already have
enough information available to determine whether or not fallback
content (ie. a "Refresh" button) is required. (Well, there still is the
question of how to detect whether JavaScript is disabled, but leaving
that aside for the moment). So, for example, the <h:selectOneMenu>
renderer could very easily check to see:
- Whether it has any ClientBehaviors attached for the "valueChange" event.
- Whether any of these ClientBehaviors are "submitting" behaviors.
In the event that:
- The <h:selectOneMenu> has a submitting valueChange ClientBehavior
attached, and...
- JavaScript is disabled
The <h:selectOneMenu> could then render its own "Refresh" button. No
need for the Behavior to do anything.
Okay, so after writing this option up, it occurs to me that not all
submitting behaviors would require fallback content to be rendered - eg.
the presence of an Ajax-based validator (case #3A above) should not
cause a "Refresh" button to appear. So if we want to leave it up to the
component renderer to provide fallback content, we would need some API
on the ClientBehavior (maybe a new hint), that the component renderer
would check to see whether fallback content is actually needed.
iv. Don't provide any automatic solution - let the application provide a
solution.
The idea here is that if we provide some easy way to determine whether
JavaScript is disabled (eg. a method on the FacesContext), page authors
who care about this can do something along the lines of:
<h:selectOneMenu>
<f:ajax/>
</h:selectOneMenu>
<!-- Provide a refresh button, but only when scripting is disabled -->
<h:commandButton value="Refresh"
rendererd="#{facesContext.scriptingDisabled}"/>
Think that's it for possible solutions.
If we want to enhance the Behavior API to support fallback content
rendering, I strongly prefer option (ii) over option (i). This solution
seems simpler, more in line with the existing ClientBehavior model, and
requires that fewer assumptions be built into Behavior implementations.
I think this is a better split of responsibilities between the component
and the Behavior (ie. component decides when/where fallback content is
needed, Behavior produces the fallback content).
BTW, an important question that we should consider is whether JSF itself
should provide a facility for detecting whether client-side scripting is
enabled. This could be very useful - ie. could allow us to solve
long-standing problems. For example, if we have a way to detect when
JavaScript is disabled, seems like we could provide an <h:commandLink>
that works when JavaScript is disabled fairly easily (fall back to an
<input type="submit">).
As for how to go about detecting whether JavaScript is disabled, I was
thinking we would do something along these lines:
- FormRenderer spits out a <script> that dynamically adds a hidden field
with some well known name.
- On postback, we check to see whether this hidden field is present
- If the hidden field is present, we know that scripting is enabled. Yay.
- If the hidden field is not present, we assume that scripting is disabled.
Of course, this means that we cannot detect whether JavaScript is
enabled until the first postback is performed. Is there some way to get
around this? Martin or Jeremy - I know you guys have provided solutions
for this sort of thing - please let jump in if you have any suggestions.
Andy
More information about the jsr-314-open-mirror
mailing list