Search code examples
javajquerytapestry

Problems encapsulating Dialog components in Tapestry 5.3


I'm trying to build a Tapestry component called CrudEntityField, based on TextField (core), DialogLink (tapestry5-jquery) and Dialog (tapestry5-jquery) components, along with Zone for AJAX updates.

My use case is quite simple:

CrudEntityField will render as a TextField. If you know the entity id, you can enter it there directly. If you don't know the id, you can click on the DialogLink button (SELECT), which pops up a Dialog window. The Dialog lists all available entities in table format, and lets us filter by some criteria. Once the user selects the entity, the text field is automatically refreshed with the related id (e.g '6'). Also, a related name/description is printed for clarity (e.g. '[Gipuzkoa]'). Snapshots: CrudEntityField, Pop-up Dialog

Ideally, all this logic can be encapsulated in a single Tapestry component. But here come the issues:

  • The dialog (<div t:type="jquery/dialog" t:clientId="dialogIdXXX">, see below) includes a search form, and is itself embedded within another form (CRUD form). As nested forms are not allowed, I could try to un-nest it. However, this approach will fail, as I get a runtime exception when the inner form is rendering, before I have the opportunity to move it.

    @AfterRender
    public void afterRender(MarkupWriter writer) {      
      Element body = writer.getDocument().find("html/body");            
      writer.getDocument().getElementById("dialogIdXXX").moveToBottom(body);        
    }
    
  • I tried other approaches, such as @HeartbeatDeferred, RenderCommand, etc. but I found no way to render the dialog out of the Form.

Component template:

<t:content>

<!-- A) Entity Field -->
<t:zone t:id="entityZone" id="zoneIdXXX">           
  <div class="inputElement">

    <t:label for="textField"/>
    <input t:id="textField"/>           

    [<t:body/>]

    <t:jquery.dialoglink t:dialog="dialogIdXXX" class="dialogLink">SELECT</t:jquery.dialoglink> 

  </div>
</t:zone>   

<!-- B) Entity Dialog -->
  <div t:type="jquery/dialog" t:clientId="dialogIdXXX" params="params" >            
   <table t:id="xa2grid" t:entity="inherit:entity" t:add="actions">         

      <p:actionsCell>       
         <a t:type="EventLink" t:event="SELECT" t:zone="zoneIdXXX" t:context="entity.id" href="#">SELECT</a>                        
      </p:actionsCell>

    </table>                    
  </div>  

</t:content>
  • Ok, so I can split the component in two pieces: EntityField (form input field) and EntityDialog (placed out of the Form, in the page template). Not ellegant, though, since this approach breaks the abstraction.

    But this approach won't work either. The zone event is now triggered by the EntityDialog component, and must be handled by itself or one of its containers. But the zone is defined in EntityField, which is now a sibling! (I can paste more code if not clear).


Solution

  • As you've seen, you can't render a form inside a form so you will need to delay the rendering of the dialog until after the form has rendered.

    So, instead of rendering the dialog inside your component, use an environmental bean. The environmental will store the config for all dialogs on the page

    http://tapestry.apache.org/environmental-services.html

    Include a block at the end of every page which looks up the config from the environment and renders all of the Dialogs. Hopefully you are using a layout so this will be in one place.

    eg: Layout.java

    @Inject
    Environment environment;
    
    @Property
    private DialogConfigs dialogConfigs;
    
    @Property
    private DialogConfig dialogConfig;
    
    void beforeRender() {
        dialogConfigs = new DialogConfigsImpl();
        environment.push(DialogConfigs.class, dialogConfigs);
    }
    
    void afterRender() {
        environment.pop(DialogConfigs.class);
    }
    

    Layout.tml

    <html>
        <body>
            <t:body/>
            <t:loop source="dialogConfigs.values" value="dialogConfig">
                <div t:type="jquery/dialog" foo="dialogConfig.foo" bar="dialogConfig.bar">  
            </t:loop>
        </body>
    </html>
    

    CrudEntityField.java

    @Environmental
    private DialogConfigs dialogConfigs;
    
    @Parameter
    private String foo;
    
    @Parameter
    private String bar;
    
    void beforeRender() {
        dialogConfigs.add(new DialogConfig(foo, bar));
    }