org.omnifaces.component.script.Highlight Maven / Gradle / Ivy
/*
* Copyright OmniFaces
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package org.omnifaces.component.script;
import static jakarta.faces.event.PhaseId.RENDER_RESPONSE;
import static java.lang.Boolean.TRUE;
import static java.lang.String.format;
import static org.omnifaces.config.OmniFaces.OMNIFACES_LIBRARY_NAME;
import static org.omnifaces.config.OmniFaces.OMNIFACES_SCRIPT_NAME;
import static org.omnifaces.util.Components.addFacesScriptResource;
import static org.omnifaces.util.Components.addScriptResource;
import static org.omnifaces.util.Components.getCurrentForm;
import static org.omnifaces.util.Events.subscribeToRequestBeforePhase;
import java.io.IOException;
import java.util.EnumSet;
import java.util.Set;
import jakarta.faces.component.FacesComponent;
import jakarta.faces.component.UIForm;
import jakarta.faces.component.UIInput;
import jakarta.faces.component.visit.VisitContext;
import jakarta.faces.component.visit.VisitHint;
import jakarta.faces.component.visit.VisitResult;
import jakarta.faces.context.FacesContext;
import org.omnifaces.util.State;
/**
*
* The <o:highlight>
is a helper component which highlights all invalid {@link UIInput} components
* and the associated labels by adding an error style class to them. Additionally, it by default focuses the first
* invalid {@link UIInput} component. The <o:highlight />
component can be placed anywhere in the
* view, as long as there's only one of it. Preferably put it somewhere in the master template for forms.
*
* <h:form>
* <h:inputText value="#{bean.input1}" required="true" />
* <h:inputText value="#{bean.input2}" required="true" />
* <h:commandButton value="Submit" action="#{bean.submit}" />
* </h:form>
* <o:highlight />
*
*
* The default error style class name is error
. You need to specify a CSS style associated with the class
* yourself. For example,
*
* label.error {
* color: #f00;
* }
* input.error, select.error, textarea.error {
* background-color: #fee;
* }
*
*
* You can override the default error style class by the styleClass
attribute:
*
* <o:highlight styleClass="invalid" />
*
*
* You can disable the default focus on the first invalid input element setting the focus
attribute.
*
* <o:highlight styleClass="invalid" focus="false" />
*
*
* Since version 2.5, the error style class will be removed from the input element and its associated label when the
* enduser starts using the input element.
*
* @author Bauke Scholtz
* @see OnloadScript
* @see ScriptFamily
*/
@FacesComponent(Highlight.COMPONENT_TYPE)
public class Highlight extends OnloadScript {
// Public constants -----------------------------------------------------------------------------------------------
/** The component type, which is {@value org.omnifaces.component.script.Highlight#COMPONENT_TYPE}. */
public static final String COMPONENT_TYPE = "org.omnifaces.component.script.Highlight";
// Private constants ----------------------------------------------------------------------------------------------
private static final Set VISIT_HINTS = EnumSet.of(VisitHint.SKIP_UNRENDERED);
private static final String DEFAULT_STYLECLASS = "error";
private static final Boolean DEFAULT_FOCUS = TRUE;
private static final String SCRIPT = "OmniFaces.Highlight.apply([%s], '%s', %s);";
private enum PropertyKeys {
// Cannot be uppercased. They have to exactly match the attribute names.
styleClass, focus
}
// Variables ------------------------------------------------------------------------------------------------------
private final State state = new State(getStateHelper());
// Init -----------------------------------------------------------------------------------------------------------
/**
* The constructor instructs Faces to register all scripts during the render response phase if necessary.
*/
public Highlight() {
subscribeToRequestBeforePhase(RENDER_RESPONSE, this::registerScriptsIfNecessary);
}
private void registerScriptsIfNecessary() {
// This is supposed to be declared via @ResourceDependency, but JSF 3 and Faces 4 use a different script
// resource name which cannot be resolved statically.
addFacesScriptResource(); // Required for jsf.ajax.request.
addScriptResource(OMNIFACES_LIBRARY_NAME, OMNIFACES_SCRIPT_NAME);
}
// Actions --------------------------------------------------------------------------------------------------------
/**
* Visit all components of the current {@link UIForm}, check if they are an instance of {@link UIInput} and are not
* {@link UIInput#isValid()} and finally append them to an array in JSON format and render the script.
*
* Note that the {@link FacesContext#getClientIdsWithMessages()} could also be consulted, but it does not indicate
* whether the components associated with those client IDs are actually {@link UIInput} components which are not
* {@link UIInput#isValid()}. Also note that the highlighting is been done by delegating the job to JavaScript
* instead of directly changing the component's own styleClass
attribute; this is chosen so because we
* don't want the changed style class to be saved in the server side view state as it may result in potential
* inconsistencies because it's supposed to be an one-time change.
*/
@Override
public void encodeChildren(FacesContext context) throws IOException {
UIForm form = getCurrentForm();
if (form == null) {
return;
}
StringBuilder clientIds = new StringBuilder();
form.visitTree(VisitContext.createVisitContext(context, null, VISIT_HINTS), (visitContext, component) -> {
if (component instanceof UIInput && !((UIInput) component).isValid()) {
if (clientIds.length() > 0) {
clientIds.append(',');
}
String clientId = component.getClientId(visitContext.getFacesContext());
clientIds.append('"').append(clientId).append('"');
}
return VisitResult.ACCEPT;
});
if (clientIds.length() > 0) {
context.getResponseWriter().write(format(SCRIPT, clientIds, getStyleClass(), isFocus()));
}
}
/**
* This component is per definiton only rendered when the current request is a postback request and the
* validation has failed.
*/
@Override
public boolean isRendered() {
FacesContext context = getFacesContext();
return context.isPostback() && context.isValidationFailed() && super.isRendered();
}
// Getters/setters ------------------------------------------------------------------------------------------------
/**
* Returns the error style class which is to be applied on invalid inputs. Defaults to error
.
* @return The error style class which is to be applied on invalid inputs.
*/
public String getStyleClass() {
return state.get(PropertyKeys.styleClass, DEFAULT_STYLECLASS);
}
/**
* Sets the error style class which is to be applied on invalid inputs.
* @param styleClass The error style class which is to be applied on invalid inputs.
*/
public void setStyleClass(String styleClass) {
state.put(PropertyKeys.styleClass, styleClass);
}
/**
* Returns whether the first error element should gain focus. Defaults to true
.
* @return Whether the first error element should gain focus.
*/
public boolean isFocus() {
return state.get(PropertyKeys.focus, DEFAULT_FOCUS);
}
/**
* Sets whether the first error element should gain focus.
* @param focus Whether the first error element should gain focus.
*/
public void setFocus(boolean focus) {
state.put(PropertyKeys.focus, focus);
}
}