Transpromo: the sequel

Before the break I wrote an entry describing how to place transpromo content on your form.  The sample was fairly restricted in that the advertisement could be inserted only in specific spots in the subform hierarchy.  Today’s sample allows the advertisement to be placed anywhere on the form. 
Note that this sample is not an interactive PDF, but is a print form — as you’d expect for a transpromo appication.  Here is the XDP form, the sample data and the result of a print operation.  In order to generate some interesting white space to work fill up, I’ve added a conditional break on the form.  Every time the date field value changes, I force a new page.

The strategy behind this sample is that we use a dynamic subform to place the advertisement on the master page rather than in the form hierarchy.  This isn’t as easy as it sounds, so I’ll explain the steps to build the form.

In a nutshell, the strategy involves:

  1. find the place on the page where the ad will fit
  2. create an instance of the advertisement subform
  3. modify the (x,y) coordinate to position the ad correctly. 

However, there are some challenges to make this work.  The main problem is that when we re-paginate, we might throw away any existing master page instances (pageAreas) and re-create them.  Any subform instances and (x,y) positions would be lost in the process.  The solution involves persisting the ad placement information in the form data and binding the advertisement subforms to the data.  The specific mechanics need a fair bit of explaining…

Processing Steps

In the previous post I described these processing steps:

  1. Loading content
  2. Merging data with template (create Form DOM)
  3. Executing calculations and validations
  4. Layout (pagination)
  5. Render

This isn’t the complete story.  There are some intermediate steps as well.  Specifically, when doing the pagination, we create pageArea objects — along with their associated content.  We then attempt to merge this page content with form data.  The expanded processing steps are:

  1. Loading content
  2. Merging data with template (create Form DOM)
  3. Executing calculations and validations
  4. Layout (pagination)
    4a. Merge data with page content
    4b. Execute calculations and validations on page content
  5. Render

Settings stored in Data

The way to customize page content at runtime is to put all the page settings in the form data and have the page content subform bind to the data.  You need to find a place in the data that will not interfere with the rest of the form data.  This is especially important if your form is based on an XML schema.  Any page data intermixed with form data would violate the schema — or could inadvertently merge with other elements in the template.  The solution is to place the page data in a separate dataset.  The default location for form data is under <xfa:data>.  We’ll place the page data under <pageData>:

<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
   <xfa:data>
       … form data goes here …
   </xfa:data>
   <pageData>  <!– data for transpromo ads goes here –> 
      <advertisement>  <!– placement data for one ad –>  
         <x>0.25in</x> <!– x,y position –> 
         <y>2.89in</y>

                       <!– bind to presence property –>   
         <presence>visible</presence>  
         <Subform7in>  <!– this will select the right sized ad –> 
                       <!– populate info/debug field –> 
            <available>7.85in</available>  
         </Subform7in>  
      </advertisement>  
      <advertisement> … next page ad … </advertisement>
   </pageData>
</xfa:datasets>

Once we generate the data, we need to make sure that the subforms on the master pages bind to the data accordingly.  The advertisement subform uses the binding expression: "!pageData.advertisement[*]". (The "!" character is a shortcut in SOM that brings you to a child of xfa.datasets).  The rest of the bindings are "Normal" (based on a name match).

Now any time a repagination happens, the page content will re-bind to the data and all the settings will be restored.

Use Dynamic Properties

Setting properties such as the x and y coordinates via data is done using dynamic properties.  Designer supports setting properties such as caption, error messages, choice list contents from from data.  While Designer exposes the set of commonly used properties, in reality almost all properties can be set this way.  e.g. To populate the x and y properties from data, I used XML source view to add the necessary <setProperty> elements:

<subform name="advertisement" w="203.2mm" layout="tb">
   <occur min="0" max="-1"/>
   <setProperty target="x" ref="$.x"/>
   <setProperty target="y" ref="$.y"/> 
   <setProperty target="presence" ref="$.presence"/>

   …
</subform>

The presence property is set to "hidden" by default so that the advertisement subforms do not clutter up the design view.  In addition to setting the x and y properties via data, we also set the subform presence property to "visible".

Note: There was one other case where I needed to use XML source view to design this form.  In order to make "advertisement" subform optional, I added the <occur> element:

<subform name="advertisement">
   <occur min="0" max="1"/>

Building the data in script

Adding data can be done using createNode() as we’ve done in previous samples.  The code to add specify the x property would look something like:

var vPageData = xfa.datasets.createNode("dataGroup","pageData");
var vAds      = xfa.datasets.createNode("dataGroup","ads");
var vAdvert   = xfa.datasets.createNode("dataGroup","advertisement");
var vX        = xfa.datasets.createNode("dataValue","x");

xfa.datasets.nodes.append(vPageData);
vPageData.nodes.append(vAds);
vAds.nodes.append(vAdvert);
vAdvert.nodes.append(vX);
vX.value = vCA.x;

Or we can use the assignNode() method and do the whole thing in one command:

xfa.datasets.assignNode("pageData.ads.advertisement.x", vCA.x, 0);

The way assignNode() works is that as it traverses the SOM expression and creates any intermediate nodes that don’t exist. In this example it would create "advertisement" as a dataGroup (since it can tell that it’s a grouping node) and then creates "x" as a dataValue (since it can tell that it is a leaf node).  Once the nodes are created, the second parameter is the value to assign to the leaf node.  The last parameter dictates how to create nodes (0 == "create/replace").

The other benefit to using assignNode() is that if your method inadvertently gets called twice, it won’t create another instance of the data.  It will overwrite the data added previously.

Triggering re-layout

In this sample we’ve placed our code in the docReady event of the adControl field.  The docReady event fires after layout and render are complete.  Once we have generated form data for each page, we add an advertisement subform (merging it with the new data) and then call xfa.layout.relayout(). I encountered a bug along the way — the relayout() call ought to have remerged without an explicit call to add the advertisement subform. Since it didn’t, the workaround was to add the subform explicitly.

Limits

1. As mentioned already, this form is intended for print.  It is not intended for (nor will it work in) an interactive environment where subforms are added or removed.  In interactive forms we can count on more layout:ready events firing.  In fact, the first sample transpromo form I made the mistake for relying too much on the layout:ready event.  Consequently, that form works only in interactive mode and not for print.  But given the greater flexibility of this second sample, I recommend staying with this approach for transpromo form design.

2. This form currently works only on page areas that have a single content area. All object positions are computed relative to the content area, but it isn’t possible to determine which content area you are currently in.

Re-Use

In order to re-use the code from this sample, simply take the content from page1 (the adholder subform) and place it on each master page where you’d like to add content.  A cautionary note: if you copy/paste the adHolder subform or if you create a custom object from it, Designer will remove the binding information. You will need to re-specify it when you bring it into a new form.

Next Steps

The sample could be extended to search for other kinds of white space.  Currently we find only the leftover vertical white space at the end of each page.  There should be enough information in the layout tree for us to also discover white space inside positioned content.

13 Responses to Transpromo: the sequel

  1. E. Loralon says:

    Hi John,I am trying to do two things with a flowed form that I have.1) On a flowed form can I force to position a field in particular place at runtime?2) Is there a way to exclude an object placed on the master from being remerged or reset? I have a form with a data field on the master page, which gets reset every time the form reloads causing the said field to loose its data.

  2. E. Loralon:1) On any given form you can modify the x,y position of a field only if it is inside a subform with positioned layout. If it’s inside a flowed subform, you have no direct control over its position. One thing you can try to do is change the size of the preceding elements to push your field into the right position. That’s the approach I used to position a subform at the bottom of a page in this blog entry.2) There is no way to exclude any elements from remerge or reset operations. If you want to preserve the value of a field on a master page, you need to either have the value bound to data, or you need to have a script on the layout:ready event to re-populate the field. Unless you do this, it’s not only remerge that could trash your value — a relayout can also make you lose your field value.John

  3. E. Loralon says:

    Thank you John for this quick reply to my question and valuable suggestions.Could you please give an example for the answer number 2 regarding the re-population?I will really appreciate it.E. Loralon

  4. E. Loralon:Probably the easiest option for you is to use data binding.Do you use it on the rest of your form?It requires that the value for your field is stored somewhere in your XML data file.Then in Designer, use the object/binding tab to specify where the field should load its data from.John

  5. E. Loralon says:

    John,With this particular form I don’t use data binding, but I will give it a try and see how it behaves.For this form I will need to use JavaScript. So I will be more than happy to get any hint that can lead me in solving this question.Thank you again John.E. Loralon

  6. E. Loralon:First of all, where binding is concerned, probably the easiest thing to do is to:a) Create another hidden field with the same name somewhere in your body pagesb) set the binding to “global”. This will keep your master page field synchronized with the field on your body page.If you want to use a calculation, try preserving the value of the field in a separate dataset.You could do this with a couple scripts:on exit save the new value:xfa.datasets.assignNode(“preserve.savedValue”, (this.rawValue == null ? “” : this.rawValue), 0);At layout:ready restore the value:this.rawValue = xfa.datasets.preserve.savedValue.value;This will preserve your master page value even during a reset operation.good luck.John

  7. E. Loralon says:

    John,I just tested the above solution and it worked fine.Thank you for this, it really did help.E. Loralon

  8. Bruce says:

    Hi John, I have found the setProperty elements to be very useful in my currect form and have successfully used them to set the w,h,x,y properties but I have been unsuccessful in setting the border.fill.color. Should this be possible? I have put the equivilent JavaScript in the ready:layout event successfully so am happy the values are OK.Also I have only been able to set the x,y, etc properties of Draw objects by placing the setProperty elements in the containing subform. Should I be able to have the setProperty elements in the Draw object themselves.Thanks again, Bruce

  9. Bruce:It should be possible. but it’s not surprising that it might be a bit tricky.The XFA syntax for border fill is:<border>  <fill>    <color value=”128,128,128″/>  </fill>  <edge presence=”hidden”/></border>The setProperty syntax that worked for me is:<setProperty target=”$.border.fill.color.value” ref=”$record.c”/>What may have thrown you is the fact that the color element has an attribute called ‘value’ which could be confused for the script property ‘value’.John

  10. Bruce says:

    Perfect explanation, that was exactly what I was doing wrong. Thanks again. Bruce

  11. Bruce says:

    Hi John, I was wondering if there was an easy way of building data that contains an attribute, maybe something like;

    xfa.datasets.assignNode(“pageData.ads.advertisement.x.@units”, “mm”, 0);

    I did do this using createNode but this seems so clumsy, I felt there must be a better way.

    var vPageData = xfa.datasets.createNode(“dataGroup”,”pageData”);
    var vAds = xfa.datasets.createNode(“dataGroup”,”ads”);
    var vAdvert = xfa.datasets.createNode(“dataGroup”,”advertisement”);
    var vX = xfa.datasets.createNode(“dataValue”,”x”);
    var vUnits = xfa.datasets.createNode(“dataValue”,”units”);
    vUnits.value = “mm”
    vX.nodes.append(vUnits)
    vUnits.contains = “metaData”;
    xfa.datasets.nodes.append(vPageData);
    vPageData.nodes.append(vAds);
    vAds.nodes.append(vAdvert);
    vAdvert.nodes.append(vX);

    Thanks

    Bruce

    • Bruce:

      Good question. Working from your example, you might try this:

      var vUnits = xfa.datasets.assignNode(“pageData.ads.advertisement.x.units”, “mm”, 0);
      vUnits.contains=”metaData”;

      John

  12. Bruce says:

    Thanks John, That is a bit easier. I now have a problem that maybe similar to E. Loralon. I have a form with a repeating field bound to my data connection. The field is on the master page and initially displays correctly. However if on body page I do a addInstance of a subform all the master page fields are cleared. Prior to adding the subform a xfa.form.remerge(); seems to work fine but afterwards it does not remerge the first value. Thanks again, Bruce