Des
Des

Reputation: 283

Problems navigating in a JSF Single Page Application

I'm learning JSF 2.2 and trying to make a Single Page Application (SPA). Using version 2.2.8. I have made a simple SPA that only displays text and pictures. I made it by using f:ajax, ui:include and ui:composition.

It all starts in the index.xhtml file:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://xmlns.jcp.org/jsf/html"
      xmlns:f="http://xmlns.jcp.org/jsf/core"
      xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
      xmlns:jsf="http://xmlns.jcp.org/jsf">
<h:head>
<title>Project S - SPA</title>
<link href="./css/styles.css" rel="stylesheet" type="text/css"/>
</h:head>
<h:body>
<h1 class="title">Project S - Single Page Application</h1>

<h:form>
    <f:ajax render="content">
        <h:commandLink value="Login" action="#{navController.setToPage('login')}"/>&nbsp;&nbsp;
        <h:commandLink value="Create user" action="#{navController.setToPage('create-user')}"/>&nbsp;&nbsp;
        <h:commandLink value="Reset password" action="#{navController.setToPage('reset-password')}"/>&nbsp;&nbsp;
    </f:ajax>
</h:form>
<hr/>
    <div jsf:id="content">
        <ui:include src="#{navController.page}.xhtml"/>
    </div>

</h:body></html>

From here I load pages like login.xhtml:

<ui:composition xmlns="http://www.w3.org/1999/xhtml"
    xmlns:h="http://xmlns.jcp.org/jsf/html"
    xmlns:ui="http://xmlns.jcp.org/jsf/facelets">
    <h1>Login page</h1>
    <h:form>
    Please log in.<br/>
    <h:panelGrid columns="2">
        User name: <h:inputText value="#{loginController.name}"/>
        Password: <h:inputSecret value="#{loginController.password}"/>
    </h:panelGrid>
        <h:commandButton value="Login" action="#{loginController.login}"/>
</h:form>
</ui:composition>

and create-user.xhtml:

<ui:composition xmlns="http://www.w3.org/1999/xhtml"
    xmlns:h="http://xmlns.jcp.org/jsf/html"
    xmlns:ui="http://xmlns.jcp.org/jsf/facelets">
    <h1>Create user page</h1>

    <h:form>
        <h:panelGrid columns="3" styleClass="formTable">
            User name:
            <h:inputText value="#{registerController.userName}"
                         id="uname"
                         required="true"
                         requiredMessage="Please enter your User Name">
            </h:inputText>
            <h:message for="uname"/>

            Password:
            <h:inputSecret value="#{registerController.password}"
                         id="pass"
                         required="true"
                         requiredMessage="Please enter your Password">
            </h:inputSecret>
            <h:message for="pass"/>

            Hint:
            <h:inputText value="#{registerController.hint}"
                         id="hint"
                         required="true"
                         requiredMessage="Please enter a hint">
            </h:inputText>
            <h:message for="hint"/>
        </h:panelGrid>
        <h:commandButton value="Register" action="#{registerController.register}"/>
    </h:form>
</ui:composition>

I use the NavController.java Managed bean to navigate them:

package controllers;

import java.io.Serializable;

import javax.faces.bean.ManagedBean;
import javax.faces.bean.ViewScoped;

@ManagedBean
@ViewScoped
public class NavController implements Serializable {
    private static final long serialVersionUID = 1L;
    private String page = "login";

    public String getPage() {
        return page;
    }

    public void setToPage(String page) {
        this.page = page;
    }
}

The pages work fine for showing up and switching to them. The first page that will load, in this case login, will work with no problem. But the other pages do not. And by do not I mean that no matter what I input when I click the h:commandButton, neither the validation is activated, nor is the user created. I just get sent back to the index.jsf page. Here is the controller class for create-user:

package controllers;

import javax.faces.bean.ManagedBean;

import database.UsersDataBaseSimulator;
import models.User;

@ManagedBean
public class RegisterController {

    private String userName, password, hint;

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getHint() {
        return hint;
    }

    public void setHint(String hint) {
        this.hint = hint;
    }

    public String register() {
        if(UsersDataBaseSimulator.USERS.containsKey(userName)) {
            return "user-exists";
        }
        User user = new User(userName, password, hint);
        UsersDataBaseSimulator.USERS.put(userName, user);
        return "user-created-successfully";
    }
}

And the controller class for the login:

package controllers;

import java.io.Serializable;

import javax.faces.bean.ManagedBean;
import javax.faces.bean.SessionScoped;

import database.UsersDataBaseSimulator;

@ManagedBean(name="loginController")
@SessionScoped
public class LoginController implements Serializable{

    private static final long serialVersionUID = 1L;
    private String name, password;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String login() {
        if(UsersDataBaseSimulator.USERS.get(name) != null) {
            if(UsersDataBaseSimulator.USERS.get(name).getPassword().equals(password)) {
                return "main-page?faces-redirect=true";
            }
        }
        return "error";
    }
}

I have created this page as a non SPA and it works fine. I've tried to switch the initial page from login to create-user and in that case only the create-user page works. At this point I don't know if this is even possible and if I am going about it the right way. I'm learning from this course http://www.coreservlets.com/JSF-Tutorial/jsf2/. So far I've finished it up to Composite Components part 1. I'll provide any additional information that you might need in order to get to the bottom of this problem. Thank you in advance to anybody that takes the time to tackle this problem.

Upvotes: 1

Views: 1951

Answers (1)

Kukeltje
Kukeltje

Reputation: 12337

I tried to make your application running by adding the User and UsersDataBaseSimulator and noticed the following:

Non-SPA (Non-Ajax even)

For the menu part you seem to be doing 'SPA' as described in How to ajax-refresh dynamic include content by navigation menu? (JSF SPA).

In the 'includes' you are effectively not doing SPA. You don't use 'ajax' on the commandButton's in the 'subpages' but return a "String" that is effectively in normal JSF navigation an outcome which in your cases don't exist. Running your application in development mode by setting javax.faces.PROJECT_STAGE in your web.xml and doing a 'failed login', you'd see:

Unable to find matching navigation case with from-view-id '/index.xhtml' for action '#{loginController.login}' with outcome 'error'.

and in the logging something like

13:07:27,933 WARNING [javax.enterprise.resource.webcontainer.jsf.application] (default task-20) JSF1064: Unable to find or serve resource, /error.xhtml.

There are two options to stay on the same page (technically being index.jsf here)

  • You could return null (or make the return void) to stay on the same page as it is but then it would not update anything, literally stay on the page as it is.
  • You could return an empty string which would make you stay on the same page but do a full page refresh. But if the page in the navigationController you'd see the same content. Having the NavigationController injected in the other page controllers and doing a 'setPage(...)' would show a new page.

The first is not what you want period and the second is sort of not 'SPA' as it does full page refreshes.

So you need ajax there too and not return real pages or outcomes (get to that later)

Ajax-Form-Navigation-Multiple-Forms bug

Now if you did all this, you'd still see the 'other pages not working when first navigating to them' part (setting breakpoints in the RegisterController#register() method, you'd see it is never called). The reason for this is that in Mojara 2.2.x there is a bug that causes this behaviour and it is related to having multiple forms (which by itself is good). When I applied this to your project, the other pages started working too besides now also giving the errors mentioned in the navigation part above.

AJAX in subpages

If in your LoginController your 'inject' the NavController and in the login() method set a (partial)page:

public class LoginController implements Serializable{

    @ManagedProperty
    NavController navController;

    ...

    public void login() {

       ... 

       navController.setPage('main-page');
    }

And in the xhtml you do

<h:commandButton value="Login" action="#{loginController.login}">
    <f:ajax render=":content"/>
</h:commandButton>

you'll get the behaviour you want. Just do this for the other pages/controllers too.

Upvotes: 1

Related Questions