Search code examples
tapestry

Tapestry does not allow dynamic components?


Using Tapestry 5.3.7 I need to create page which contains multiple dynamic blocks.

Example page tml:

<t:form>
    <t:loop source="chosenBlockIds" value="blockId">
        <t:delegate to="convertBlockIdToBlockObject(blockId)" myBlockId="${blockId}"/>
    </t:loop>

    <t:block id="blockA">
       several text fields and select components here with zones
       <t:textfield t:id="text1" value="dtoMap(blockId).text1">...
       <t:select t:id="select1" t:zone="zone1" value="dtoMap(blockId).select1" ...>
       <t:zone t:id="zone1" id="zone1" ...>
           <t:select t:id="select2" value="dtoMap(blockId).select2" ...>
       </t:zone>
    </t:block>

    <t:block id="blockB">
       several text fields and select components here with zones
    </t:block>

    <t:block id="blockC">
       several text fields and select components here with zones
    </t:block>

</t:form>

On previous page user will choose from a list of possible blocks, which blocks to display. Each block can be chosen multiple times, meaning e.g. blockA can be rendered two times. This means the blockId can't be simply as blockA, but must be unique value like blockA_1, blockA_2. I have managed to create such values in chosenBlockIds list. In delegate, the method convertBlockIdToBlockObject will parse the blockId, e.g. from blockA_2 will get blockA, and return Block object corresponding to blockA. This is all working and page is rendering correctly.

Each component's value is bound to a DTO class which contains fields for text1, select1, select2 etc. Each DTO instance is stored in a Map<String, DTO> in the page class. Map key is the blockId.

Let's assume user selected blockA 2x. Because the blockA is present on the page 2x, each block and all components in the block need to know the block's unique blockId. I tried to use informal parameter myBlockId on delegate in hopes to propagate the value as render variable. But Tapestry does not allow using render variable as input to any components in the block. How to transfer blockId to all components in the block?

Now the problem happens when I submit the form or work with Ajax, the values from blockA_2 are mixed into blockA_1, which is the problem to solve. Also the select2 is in a zone and is refreshed depending on selected value in select1 using Ajax. Incorrectly, when I choose a value in select1 in blockA_2, it will update select2 in blockA_1. The code in page class contains declared field Zone zone1 and when value is changed in select1, using onValueChanged method it will return zone1.getBody(). But I need zone1 2x, one for blockA_1 and second for blockA_2, but because zone is declared as field, obviously I can't declare zone fields dynamically. Looks like unsolvable problem in Tapestry? I need two instances of zone1 (to be created dynamically), probably stored in another map with blockId as key.

Page class:

    @Property
    @Persist
    private String blockId;

    @Property
    @Persist
    private Map<String, DTO> dtoMap;

    @Inject
    private Block blockA;
    @Inject
    private Block blockB;
    @Inject
    private Block blockC;

    @Component
    private Zone zone1;

    public Object onValueChanged(EventContext context) {
        String blockId = context.get(String.class, 1);
        // I need to return zone1 per blockId, but I can't declare fields dynamically
        return zone1.getBody();
    }

Solution

  • What you explained is doable, but you need to change few things:

    1. Whenever you put form fields in a loop you need to use t:SubmitNotifier, otherwise submitted values may be overridden from previous iteration. Remember, Tapestry has static structure. Example in Jumpstart http://jumpstart.doublenegative.com.au/jumpstart/examples/ajax/formlooptailored1

    2. In onValueChanged, instead of returning body per zone use AjaxResponseRenderer#addRender(String clientId, Object renderer) to render concrete block into predefined client-side zone, i.e.:

       if (request.isXHR())
       {
           ajaxResponseRenderer.addRender(getZoneIdFor(blockId), getBlock(blockId));
       }
      

      Here getBlock() can returns one of the actual blocks defined in TML, note your t:Block need to have t:id in order to be injectable:

      @Inject Block blockA;
      @Inject Block blockB;
      
      public getBlock(String blockId)
      {
          if (blockId.startsWith("blockA") return blockA;
          // etc.
      }
      
    3. To implement pt.2 you need all your t:Zone components have explicit id="{getZoneIdFor(blockId)}", not just t:id -- it can be any string, but you need to make sure it's always the same for your blockId.