1

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.

Des
  • 213
  • 1
  • 3
  • 14
  • In my opinion, JSF is not very well suited for single-page applications. If you really want an SPA, then one of the many front-end frameworks (Angular, React, Vue, ... and hundreds of others) is most likely a better choice. JSF was invented in a time when SPA's were not really a thing yet. So it might be possible, but it's not really what JSF was designed for, which means it's not going to be easy. – Jesper Jan 26 '19 at 22:15
  • @Jesper This is what I was afraid of. Unfortunately I kind of have to do it in JSF. First of because that's my assignment, second because Java is the only language that I reasonably know. I have just started learning JavaScript and learning frameworks are a long way away for me right now. – Des Jan 26 '19 at 22:39
  • 2
    JSF can be used for SPA and afaics, the approach you've taken is the right on. Can't see something being the cause of what you experience but I cannot run it either. Can you reduce code and make it a real [mcve]. Then I'm more than happy to investigate further. Oh and post jsf version and implementation please – Kukeltje Jan 27 '19 at 07:24
  • Off-topic: if you are starting from scratch, I suggest using cdi managed beans (`@Named`) instead of the deprecated `@ManagedBean` – Kukeltje Jan 27 '19 at 07:26
  • @Kukeltje Thank you for the advice. Can you tell me what you mean by implementation? – Des Jan 27 '19 at 11:03
  • Mojarra or MyFaces – Kukeltje Jan 27 '19 at 11:16
  • I am almost certain that it' Mojarra. The jar was prepacked in the Eclipse Web Project Template that I downloaded from the site that I'm learning from: http://www.coreservlets.com/JSF-Tutorial/jsf2/ and I just use that. While I have you here. When I try to change `@ManagedBean` to `@Named` Eclipse does not offer any suggestion for it unlike for `@ManagedBean` where it just shows it from `javax.faces.bean`. Why? – Des Jan 27 '19 at 11:28
  • Hmm ... it might be because I'm using Tomcat, and not a "true Java EE container". – Des Jan 27 '19 at 11:39
  • You can use cdi as well in Tomcat. There are even Q/A on it in Stackoverflow. But buying the JSF 2.3 book by Bauke Scholtz and Arjan Thijms is money well spent https://www.apress.com/in/book/9781484233863 – Kukeltje Jan 27 '19 at 13:08

1 Answers1

1

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.

Kukeltje
  • 12,223
  • 4
  • 24
  • 47
  • Oh wow! Thank you so much for taking the time to do all this. I really means a lot. I want to ask you something that is really off topic. I'm looking to get a job, is it a smart path to keep on going with JSF or should I start learning Spring and Angular/React? The more I learn about JSF the more I feel as though it's not being used any more and people don't like it. I would need to learn JS from scratch but at this point it steams better than sticking to something that nobody is looking for. – Des Jan 27 '19 at 13:20
  • 1
    @Des you got quite a few answers and pretty quickly for a framework that nobody is using anymore ;) – MitchBroadhead Jan 27 '19 at 18:22
  • @MitchBroadhead Yeah, I'm a happy panda :D, except for the scary part of "a framework that nobody is using anymore". I'm doing this to get a job and I'm afraid that I've been led to a sub optimal path. I'm thinking about starting over but I'm not sure about the path. On the one hand, since I have reasonable knowledge with Java, I would like to go with Spring and Hibernate. But I'm constantly frustrated that I don't know JavaScript. What would you suggest? – Des Jan 27 '19 at 18:37
  • 1
    The scary part that angular will be superceeded by yet another version that is not backwards compatible? Or that react will turn out to be not as good as knockout 6 or? For serious business applications, JSF is still great and modern (html5, css3+, ajax, 'SPA' (hyped!). And JSF should be compared with one of the javascript frameworks + A + B + C + since it does more than just the front-end ('OWASP' like things are build-in) And with e.g. AngularFaces you can have best of both worlds. But PrimeFaces component framework, which is jquery based has lots of components and lots of serious users. – Kukeltje Jan 27 '19 at 19:36
  • 1
    And looking at the number of new questions in here (or anywhere on the internet) is not a good indication of number of users. It might as well be that the framework is unclear, unstable, un... Less new questions might also indicate stability since all has been answered (in JSF there are many 'duplicates'). This is not an advice since I'm biased but many 'popular' blog sites make money by posting things that cause turmoil, or excite, not always the best reasons for comparing things. – Kukeltje Jan 27 '19 at 19:37
  • Thank you @Kukeltje. To be honest I was scared (and still am) by all the "JSF is dead" talk. As somebody who doesn't know JavaScript, starting from scratch feels like a daunting task. As a matter of fact, I gave up last night and went to learn Spring, since at least there I know Java to start with. Thank you for your feedback, it has been very informative, a bit of a roller coaster :) but informative. – Des Jan 28 '19 at 08:11
  • @Des The "JSF is dead" talk might be sourced from older JSF1 versions. If JSF was dead in version 1, it was reborn with version 2. If your question is answered please mark it answered. – Selaron Jan 28 '19 at 21:01
  • JSF2.3 with PrimeFaces, OmniFaces, Deltaspike and OptimusFaces is great. Responsive, performant, quick development for business apps – Kukeltje Jan 28 '19 at 21:22
  • @Selaron I can't confirm the question as answered because I have tried implementing the solution that I was provided and I can't make it work. Problem is that I don't know if the answer is not right or if I am not knowledgeable enough to implement it properly. If at any point I resolve this problem, and the provided answer was the way that I did so, I will make sure to mark it. – Des Jan 29 '19 at 06:51
  • That's OK. @Des – Selaron Jan 29 '19 at 07:15