JSF Modal Goodness

| Comments

Meet the new web. The simpler the better. The cleaner the better. The more pleasing the colors the better. The fuzzier feelings the better. The latest fuzzies have been brought on by a slew of modals. Previously, we haven’t used many modals in our layouts and designs, so here’s the first working pattern on how to get this kind of stuff working.

This solution allows content to appear in a modal, validation errors to post back to the modal, edits made in a modal to persist back to the database, and success messages to appear on parent page.

Meet the new web. The simpler the better. The cleaner the better. The more pleasing the colors the better. The fuzzier feelings the better. The latest fuzzies have been brought on by a slew of modals. Previously, we haven’t used many modals in our layouts and designs, so here’s the first working pattern on how to get this kind of stuff working.

This solution allows:

  • Content to appear in a modal
  • Validation errors post back to the modal
  • Edits made in a modal persist back to the database
  • Success messages appear on parent page

Example uses these technologies:

  • JSF/Facelets
  • Spring 2.5
  • JPA/Hibernate
  • Hibernate validators
  • Seam
  • JQuery

This includes these pieces:

  • Parent JSF
  • JQuery for Modal - using jQuery and jqModal
  • Modal JSF
  • Seam pages.xml for navigation
  • View bean
  • Model object

Overall, the flow occurs as follows:

  • Nav to parent page
  • Trigger modal with link
  • Modal appears
  • Content is loaded in modal
  • Submissions occur via AJAX to the view bean
  • View bean logic and Seam determine outcome
  • If errors, AJAX re-renders in modal
  • If save successful, parent page is re-rendered with message

The Parent Page (person-info-personal.xhtml)

Javascript for Modal:

1
2
3
4
5
6
7
8
9
10
11
<script type="text/javascript" src="#{request.contextPath}/scripts/jquery.pack.js"></script>
<script type="text/javascript" src="#{request.contextPath}/scripts/jqModal.pack.js"></script>
<script type="text/javascript">
    jQuery(document).ready(function() {
        jQuery('#modal-edit-personal').jqm({
            ajax: '#{request.contextPath}/person-info-personal-edit.jsf?personId=#{personDetailsBean.person.id}&amp;conversationId=#{conversation.id}&amp;time=#{personDetailsBean.currentMillis}',
            trigger: '#btn-edit-personal',
            modal: true
        });
    })
</script>

Note:

  • The Seam conversation object is just available. No convenience method required to retrieve it.
  • Additional url parameter required to get around an IE caching issue. We just need a unique value so that the AJAX request isn’t assumed cached by IE. In this case, I used the current time in milliseconds (Method on view bean).

Trigger for Modal:

1
<a id="btn-edit-personal" class="edit-link-icon edit-link float-right" href="#">Edit</a>

Modal Container:

1
<div id="modal-edit-personal" class="jqmWindow smallWindow"></div>

Note: - Just include this right before the end of the html body.

Style for Modal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.smallWindow {components.css (line 105)
  margin-left:-250px;
  top:15%;
  width:500px;
}
.jqmWindow {components.css (line 84)
  background-color:#FFFFFF;
  border:5px solid #DDDDD3;
  color:#333333;
  display:none;
  left:50%;
  margin-left:-375px;
  padding:5px;
  position:fixed;
  top:8%;
  width:750px;
  z-index:3000 !important;
}

Success Validation Message

1
2
3
4
5
<h:panelGroup rendered="#{param['saveSuccess'] ne null}">
    <div id="edit-success" class="alert" style="display: block;">
        <p>Success: Changes saved for #{personDetailsBean.person.name.fullName}.</p>
    </div>
</h:panelGroup>

Note:

  • The parameter to determine ability to render is just a param[]. It’s not bound to the bean, which means it disappears on subsequent requests, which is what we want.

Seam Navigation

pages.xml Entry for modal:

1
2
3
4
5
6
7
8
9
10
11
12
13
<page view-id="/person-info-personal-edit.xhtml">
    <begin-conversation join="true" flush-mode="MANUAL"></begin-conversation>
    <param name="personId" value="#{personDetailsBean.personId}"></param>
    <action execute="#{personDetailsBean.loadPerson}" if="#{personDetailsBean.person eq null}"></action>
    <navigation>
        <rule if-outcome="saved-person-edit">
            <redirect view-id="/person-info-personal.xhtml">
                <param name="personId" value="#{param['personId'] != null ? param['personId'] : personId}"></param>
                <param name="saveSuccess" value="true"></param>
            </redirect>
        </rule>
    </navigation>
</page>

pages.xml Entry for parent:

1
2
3
4
5
6
<page view-id="/person-info-personal.xhtml">
  <begin-conversation join="true" flush-mode="MANUAL"></begin-conversation>
  <param name="personId" value="#{personDetailsBean.personId}"></param>
  <action execute="#{personDetailsBean.loadPerson}" if="#{personDetailsBean.person eq null}"></action>
  <!-- ... -->
</page>

Note:

  • The “saveSuccess” parameter is the key to showing the success message on the parent page.

The Modal Page (person-info-personal-edit.xhtml)

Modal Markup:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<a4j:form id="info-edit-form" ajaxSubmit="true" reRender="alerts">

    <a4j:outputPanel id="alerts">
        <ldsp:alerts />
    </a4j:outputPanel>

    <div class="target">
        <h2>Edit Personal Information</h2>

        <p>Please make the necessary changes and press "Save" when finished.</p>

        <div id="notes">
            <dl>
                <dt>Official Name</dt>
                <dd>
                    #{personDetailsBean.person.name.fullName}
                </dd>
            </dl>
            <dl class="short-inputs">
                <dt>Preferred Name</dt>
                <dd>
                    <h:inputText styleClass="input-text pref-name-field" value="#{personDetailsBean.person.preferredFirstName}" required="true" requiredMessage="Preferred first name required." />
                </dd>
                <dd>
                    <h:inputText styleClass="input-text pref-name-field" value="#{personDetailsBean.person.preferredMiddleName}" required="true" requiredMessage="Preferred first name required." />
                </dd>
                <dd>
                    <h:inputText styleClass="input-text pref-name-field" value="#{personDetailsBean.person.preferredLastName}" required="true" requiredMessage="Preferred first name required." />
                </dd>
            </dl>
            <dl>
                <dt>Blood Type</dt>
                <dd>
                    <h:selectOneMenu value="#{personDetailsBean.person.bloodType}">
                        <f:selectItem value="#{null}" itemLabel="None given" />
                        <f:selectItems value="#{personDetailsBean.bloodTypesList}" />
                    </h:selectOneMenu>
                </dd>
            </dl>

            <!--[if IE 6]>
            <dl>
            <dt>&nbsp;</dt>
            <dd>&nbsp;</dd>
            </dl><![endif]-->

            <p class="buttons">
                <h:commandButton action="#{personDetailsBean.savePerson}" value="Save" styleClass="button-default" />
                <input type="button" class="button-nondefault jqmClose" value="Cancel" />
            </p>
        </div>
    </div>
</a4j:form>

Note:

  • <ldsp:alerts /> - custom component to display Alerts.
  • The ajaxSubmit attribute on the a4j:form must use a capital S for ‘Submit’. (My code renderer on this page is lowercasing that for some reason.) Same with the ‘r’ on ‘reRender’.
  • Server-side Logic

    Java Model:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    @Entity
    public class Person implements Serializable {
        @Column(name = "PREF_FIRST")
        private String preferredFirstName;
    
        @Column(name = "PREF_MIDDLE")
        private String preferredMiddleName;
    
        @Column(name = "PREF_LAST")
        private String preferredLastName;
    
        @Column(name = "BLOOD_TYPE")
        private String bloodType;
    
        /** ... */
    }
    

    View Bean:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    @Controller
    @Scope("conversation")
    @SuppressWarnings("serial")
    public class PersonDetailsBean implements Serializable {
       public String savePerson() {
           String retval = null;
           try {
                personDetailsService.modifiyPerson(getPerson()); // saves missionary
                retval = "saved-person-edit";
           } catch (OptimisticLockException e) {
                Alert.addErrorAlert("Optimistic Lock: The object was modified out from under you.");
           }
           return retval;
       }
       // Method to circumvent IE caching issue
       public long getCurrentMillis() {
           return new Date().getTime();
       }
       /* ... */
    }
    

Comments