Reusable Modal

Here we have created a modal dialog component that can be used around any content.

Id
1
Version
90
First Name
Lost
Last Name
eeeeasdas
Region
West Coast
Start Date
Jun 4, 2026
Update...

The component is called SimpleModal. Its job is to show or hide a Bootstrap Modal, with our content inside it.

References: Bootstrap Modal, Tapestry JavaScript, JavaScriptSupport, RequireJS, jQuery API.

Home

The source for PersonEditor is shown in the Smaller Components CRUD example.
The source for IPersonFinderServiceLocal and @EJB is shown in the Session Beans and @EJB examples.


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<!-- We need a doctype to allow us to use special characters like &nbsp; 
     We use a "strict" DTD to make IE follow the alignment rules. -->
     
<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_4.xsd">
<body class="container">
    <h3>Reusable Modal</h3>

    <noscript class="js-required">
        ${message:javascript_required}
    </noscript>
    
    <p>Here we have created a modal dialog component that can be used around any content.</p>

    <div class="eg">

        <t:zone t:id="paneZone" id="modalExamplePaneZone">
    
            <t:if test="isFunction('review')">
                <t:beandisplay object="person"/>
                <t:eventlink event="toUpdate" context="personId" async="true">Update...</t:eventlink>
            </t:if>
            
        </t:zone>

        <t:zone t:id="modalZone" id="modalExampleModalZone">

            <t:if test="isFunction('update')">
                <t:SimpleModal t:id="personUpdateModal">
                    <t:together.ajaxcomponentscrud.PersonUpdate t:id="personUpdate" personId="personId"/>
                </t:SimpleModal>
            </t:if>
        
        </t:zone>
        
    </div>
    
    <p>The component is called SimpleModal. Its job is to show or hide a Bootstrap Modal, 
        with our content inside it.</p>

    References: 
    <a href="http://getbootstrap.com/javascript/#modals">Bootstrap Modal</a>, 
    <a href="http://tapestry.apache.org/javascript.html">Tapestry JavaScript</a>, 
    <a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/services/javascript/JavaScriptSupport.html">JavaScriptSupport</a>, 
    <a href="http://requirejs.org">RequireJS</a>, 
    <a href="http://api.jquery.com">jQuery API</a>.<br/><br/> 
    
    <t:pagelink page="Index">Home</t:pagelink><br/><br/>
    
    The source for PersonEditor is shown in the Smaller Components CRUD example.<br/>
    The source for IPersonFinderServiceLocal and @EJB is shown in the Session Beans and @EJB examples.<br/><br/>
    
    <t:tabgroup>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/pages/examples/javascript/ReusableModal.tml"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/pages/examples/javascript/ReusableModal.java"/>
        <t:sourcecodetab src="/web/src/main/resources/META-INF/assets/css/examples/reusablemodal.css"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/components/SimpleModal.tml"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/components/SimpleModal.java"/>
        <t:sourcecodetab src="/web/src/main/resources/META-INF/modules/simple-modal.js"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/components/together/ajaxcomponentscrud/PersonUpdate.tml"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/components/together/ajaxcomponentscrud/PersonUpdate.java"/>
    </t:tabgroup>
</body>
</html>


package jumpstart.web.pages.examples.javascript;

import javax.ejb.EJB;

import jumpstart.business.domain.person.Person;
import jumpstart.business.domain.person.iface.IPersonFinderServiceLocal;
import jumpstart.web.components.SimpleModal;

import org.apache.tapestry5.annotations.Import;
import org.apache.tapestry5.annotations.InjectComponent;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.corelib.components.Zone;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.services.Request;
import org.apache.tapestry5.services.ajax.AjaxResponseRenderer;

@Import(stylesheet = "css/examples/reusablemodal.css")
public class ReusableModal {

    public enum Function {
        REVIEW, UPDATE;
    }

    // The activation context

    @Property
    private Long personId;

    // Screen fields

    @Property
    private Person person;

    // Work fields

    private Function function;

    // Generally useful bits and pieces

    @InjectComponent
    private Zone paneZone;

    @InjectComponent
    private Zone modalZone;

    @InjectComponent
    private SimpleModal personUpdateModal;

    @EJB
    private IPersonFinderServiceLocal personFinderService;

    @Inject
    private Request request;

    @Inject
    private AjaxResponseRenderer ajaxResponseRenderer;

    // The code

    void onActivate(Long personId) {
        this.personId = personId;
    }

    Long onPassivate() {
        return personId;
    }

    public void setupRender() {
        function = Function.REVIEW;
        populateBody();
    }

    void onToUpdate(Long personId) {
        this.personId = personId;
        function = Function.UPDATE;

        if (request.isXHR()) {
            ajaxResponseRenderer.addRender(modalZone);
        }
    }

    void onCanceledFromPersonUpdate(Long personId) {
        this.personId = personId;
        function = Function.REVIEW;

        personUpdateModal.hide();

        populateBody();

        if (request.isXHR()) {
            ajaxResponseRenderer.addRender(paneZone);
        }
    }

    void onUpdatedFromPersonUpdate(Person person) {
        this.personId = person.getId();
        function = Function.REVIEW;

        personUpdateModal.hide();

        populateBody();

        if (request.isXHR()) {
            ajaxResponseRenderer.addRender(paneZone);
        }
    }

    public void populateBody() {

        // Get person with id 1 - ask business service to find it (from the
        // database).

        person = personFinderService.findPerson(1L);

        if (person == null) {
            throw new IllegalStateException("Database data has not been set up!");
        }
    }

    public boolean isFunction(Function function) {
        return function == this.function;
    }

}


.eg {
                margin: 20px 0;
                padding: 14px;
                border: 1px solid #ddd;
                border-radius: 6px;
                -webkit-border-radius: 6px;
                -mox-border-radius: 6px;
}

.eg .simple-modal .modal-dialog .close {
                position: absolute;
                top: 6px; right: 16px;
}

.eg .simple-modal .modal-dialog .modal-body h1 {
                text-align: center;
                margin: 0 0 20px;
                border-bottom: 1px solid #ddd;
                font-size: 24px;
                line-height: 40px;
}

.js-required {
                color: red;
                display: block;
                margin-bottom: 14px;
}



<!-- Based on http://readyareyou.blogspot.com.au/2012/11/tapestry5-bootstrap-modal-dialog.html . -->

<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_4.xsd">

<t:content>
    <div id="${clientId}" class="modal simple-modal fade" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">  
        <div class="modal-dialog">
            <div class="modal-content">
                <div class="modal-body">
                    <t:body/>
                    <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
                </div>  
            </div>
        </div>  
    </div> 
</t:content>

</html>



// Based on http://readyareyou.blogspot.com.au/2012/11/tapestry5-bootstrap-modal-dialog.html .

package jumpstart.web.components;

import javax.inject.Inject;

import org.apache.tapestry5.BindingConstants;
import org.apache.tapestry5.ClientElement;
import org.apache.tapestry5.annotations.Parameter;
import org.apache.tapestry5.json.JSONObject;
import org.apache.tapestry5.services.Request;
import org.apache.tapestry5.services.ajax.AjaxResponseRenderer;
import org.apache.tapestry5.services.ajax.JavaScriptCallback;
import org.apache.tapestry5.services.javascript.JavaScriptSupport;

public class SimpleModal implements ClientElement {

    // Parameters

    @Parameter(name = "componentClientId", value = "prop:componentResources.id", defaultPrefix = BindingConstants.LITERAL)
    private String componentClientId;

    // Generally useful bits and pieces

    @Inject
    private JavaScriptSupport javaScriptSupport;

    @Inject
    private AjaxResponseRenderer ajaxResponseRenderer;

    @Inject
    private Request request;

    // The code

    @Override
    public String getClientId() {
        return componentClientId;
    }

    void afterRender() {
        javaScriptSupport.require("simple-modal").invoke("activate").with(componentClientId, new JSONObject());
    }

    public void hide() {
        if (request.isXHR()) {
            ajaxResponseRenderer.addCallback(makeScriptToHideModal());
        }
    }

    private JavaScriptCallback makeScriptToHideModal() {
        return new JavaScriptCallback() {
            public void run(JavaScriptSupport javascriptSupport) {
                javaScriptSupport.require("simple-modal").invoke("hide").with(componentClientId);
            }
        };
    }

}


define(["jquery", "bootstrap/modal"], function($) {
    
    var activate = function(modalId, options) {
        $('#' + modalId).modal(options);
    }
    
    var hide = function(modalId) {
        var $modal = $('#' + modalId);

        if ($modal.length > 0) {
            // Hide will trigger removal.
            $modal.modal('hide');
        }
        else {
            // The modal's already gone, but the backdrop may still be there.
            $('body').removeClass('modal-open');
            $('.modal-backdrop').remove();
        }
    }

    return {
        activate : activate,
        hide : hide
    }
    
});


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<!-- We need a doctype to allow us to use special characters like &nbsp; 
     We use a "strict" DTD to make IE follow the alignment rules. -->
     
<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_4.xsd" xmlns:p="tapestry:parameter">
<t:content>

    <h1>Update</h1>
    
    <t:zone t:id="formZone" id="formZone">
        <t:form t:id="form" class="form-horizontal" validate="person" context="personId" async="true">
        
            <t:errors globalOnly="true"/>
        
            <t:if test="person">
                <!-- If optimistic locking is not needed then comment out this next line. It works because Hidden fields are part of the submit. -->
                <t:hidden value="person.version"/>
                
                <t:together.smallercomponentscrud.PersonEditor person="person"/>
    
                <div class="form-group">
                    <div class="col-sm-4 col-sm-offset-4">
                        <t:submit value="Save" />
                        <t:eventlink event="cancel" context="personId" class="btn btn-default" async="true">Cancel</t:eventlink>
                    </div>
                </div>
            </t:if>
    
            <t:if test="!person">
                Person ${personId} does not exist.<br/><br/>
            </t:if>
        </t:form>
    </t:zone>
                
</t:content>
</html>


package jumpstart.web.components.together.ajaxcomponentscrud;

import javax.ejb.EJB;

import jumpstart.business.domain.person.Person;
import jumpstart.business.domain.person.iface.IPersonFinderServiceLocal;
import jumpstart.business.domain.person.iface.IPersonManagerServiceLocal;
import jumpstart.util.ExceptionUtil;

import org.apache.tapestry5.ComponentResources;
import org.apache.tapestry5.annotations.Events;
import org.apache.tapestry5.annotations.InjectComponent;
import org.apache.tapestry5.annotations.Parameter;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.corelib.components.Form;
import org.apache.tapestry5.corelib.components.Zone;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.services.Request;
import org.apache.tapestry5.services.ajax.AjaxResponseRenderer;

/**
 * This component will trigger the following events on its container (which in this example is the page):
 * {@link PersonUpdate#CANCELED}(Long personId), {@link PersonUpdate#UPDATED}(Long personId).
 */
// @Events is applied to a component solely to document what events it may
// trigger. It is not checked at runtime.
@Events({ PersonUpdate.CANCELED, PersonUpdate.UPDATED })
public class PersonUpdate {
    public static final String CANCELED = "canceled";
    public static final String UPDATED = "updated";

    // Parameters

    @Parameter(required = true)
    @Property
    private Long personId;

    // Screen fields

    @Property
    private Person person;

    // Generally useful bits and pieces

    @EJB
    private IPersonFinderServiceLocal personFinderService;

    @EJB
    private IPersonManagerServiceLocal personManagerService;

    @InjectComponent
    private Form form;

    @InjectComponent
    private Zone formZone;

    @Inject
    private Request request;

    @Inject
    private ComponentResources componentResources;

    @Inject
    private AjaxResponseRenderer ajaxResponseRenderer;

    // The code

    boolean onCancel(Long personId) {
        this.personId = personId;

        componentResources.triggerEvent(CANCELED, new Object[] { personId }, null);
        // We don't want the event to bubble up, so we return true to say we've
        // handled it.
        return true;
    }

    void onPrepareForRender() {

        // If fresh start, make sure there's a Person object available.

        if (form.isValid()) {
            person = personFinderService.findPerson(personId);
            // Handle null person in the template.
        }
    }

    void onPrepareForSubmit(Long personId) {
        this.personId = personId;

        // Get objects for the form fields to overlay.
        person = personFinderService.findPerson(personId);

        if (person == null) {
            form.recordError("Person has been deleted by another process.");
            // Instantiate an empty person to avoid NPE in the Form.
            person = new Person();
        }
    }

    boolean onValidateFromForm() {

        if (form.getHasErrors()) {
            return true;
        }

        try {
            person = personManagerService.changePerson(person);
        }
        catch (Exception e) {
            // Display the cause. In a real system we would try harder to get a
            // user-friendly message.
            form.recordError(ExceptionUtil.getRootCauseMessage(e));
        }

        return true;
    }

    boolean onSuccess() {
        // We want to tell our containing page explicitly what person we've updated, so we trigger a new event with a
        // parameter. It will bubble up because we don't have a handler method for it.
        componentResources.triggerEvent(UPDATED, new Object[] { person }, null);

        // We don't want the original event to bubble up, so we return true to
        // say we've handled it.
        return true;
    }

    boolean onFailure() {

        if (request.isXHR()) {
            ajaxResponseRenderer.addRender(formZone);
        }

        // We don't want the event to bubble up, so we return true to say we've
        // handled it.
        return true;
    }
}