3

I have a requirement to create a javascript function that when invoked will save all of the valid components on a JSF 2.0 form. Since the complete form will never be valid as a whole I need to figure out a way to run the lifecycle per component so that if the validation phase is successful the model will be updated and eventually saved.

Ideally, this needs to be a single ajax request as iterating over each component with a separate ajax request would be painfully inefficient.

Has anyone solved the problem before? If not could you give me some pointers on possible implementations?

Edit:

Here's what I have that seems to be working well so far:

@ManagedBean(name = "partialAppSaveBean")
@RequestScoped
public class PartialAppSaveBean implements Serializable {

    protected static final Logger LOGGER = LoggerFactory.getLogger(PartialAppSaveBean.class);
    private static final long serialVersionUID = 1L;

    /**
     * Save any valid Application values
     *
     * @param event
     */
    public void saveData(AjaxBehaviorEvent event) {
        final FacesContext context = FacesContext.getCurrentInstance();

        UIForm form = getParentForm(event.getComponent());
        Set<VisitHint> hints = EnumSet.of(VisitHint.SKIP_UNRENDERED);

        form.visitTree(VisitContext.createVisitContext(context, null, hints), new VisitCallback() {

            @Override
            public VisitResult visit(VisitContext context, UIComponent component) {
            if (component instanceof UIInput) {
                UIInput input = (UIInput) component;
                input.validate(context.getFacesContext());

                if (input.isValid() && input.getValue() != null) {
                    ValueExpression valueExpression = input.getValueExpression("value");

                    if (valueExpression != null
                            && valueExpression.getExpressionString() != null) {
                        try {
                            valueExpression.setValue(context.getFacesContext().getELContext(), input.getValue());
                        } catch (Exception ex) {
                            LOGGER.error("Expression [ " + valueExpression.getExpressionString() + 
                                    "] value could not be set with value [" + input.getValue() + "]", ex);
                        }                            
                    }
                }
            }

            return VisitResult.ACCEPT;
            }
        });

        //Save data here
    }

    /**
     * Returns the parent form for this UIComponent
     * 
     * @param component
     * @return form
     */
    private static UIForm getParentForm(UIComponent component) {
        while (component.getParent() != null) {
            if (component.getParent() instanceof UIForm) {
                return (UIForm) component.getParent();
            } else {
                return getParentForm(component.getParent());
            }
        }

        return null;
    }

}

Invoked with something like:

<h:commandButton
    id="saveData">
    <f:ajax listener="#{partialAppSaveBean.saveData}" execute="@form" immediate="true" onevent="onPartialSave" />
</h:commandButton>
Dave Maple
  • 8,102
  • 4
  • 45
  • 64

1 Answers1

2

You could use UIComponent#visitTree() on the UIForm.

FacesContext context = FacesContext.getCurrentInstance();
UIForm form = getFormSomehow();
Map<String, Object> validValues = new HashMap<String, Object>();
Set<VisitHint> hints = EnumSet.of(VisitHint.SKIP_UNRENDERED);

form.visitTree(VisitContext.createVisitContext(context, null, hints), new VisitCallback() {

    @Override
    public VisitResult visit(VisitContext context, UIComponent component) {
        if (component instanceof UIInput) {
            UIInput input = (UIInput) component;

            if (input.isValid()) {
                validValues.put(input.getClientId(context.getFacesContext()), input.getValue());
            }
        }

        return VisitResult.ACCEPT;
    }
});
BalusC
  • 1,082,665
  • 372
  • 3,610
  • 3,555
  • Nice. If input.isValid() is there an easy way to convert the value and update the model the component is bound to? – Dave Maple May 24 '12 at 17:45
  • Hmm that depends on the functional requirements. Is the form already submitted/invalidated beforehand and you're thus doing `` only? Or do you want to submit and process the entire form anyway and takeover JSF validation? If the latter, invoke `input.validate(context.getFacesContext())` before `if (input.isValid())`. This will by the way also perform the conversion. It won't update the model values, so you'd have to grab it by `input.getValue()`. – BalusC May 24 '12 at 17:50
  • The requirement is that when the user attempts to exit i would use the onbeforeunload method to prompt them if they want to save their data (even if it's incomplete) so I would be invoking the equivalent of . Then I need to update the model of the valid UIInput elements and save the model hierarchy. – Dave Maple May 24 '12 at 17:56
  • 1
    I see. This is by the way an interesting idea for OmniFaces. – BalusC May 24 '12 at 18:00
  • That would be a most excellent addition to an already excellent project. I think the blueprint you've provided should work for what I need today. Time to go lay down some code. – Dave Maple May 24 '12 at 18:18
  • Should I return VisitResult.COMPLETE once I've finished my routine? – Dave Maple May 24 '12 at 21:32
  • Oops, I forgot it. I updated the answer. See also http://docs.oracle.com/javaee/6/api/javax/faces/component/visit/VisitResult.html – BalusC May 24 '12 at 21:36