This article is meant as a sequel to a previous article on basic XForms Tables. You can read the first article here
. The first article outlines how to build a very simple Lotus Forms XForms table with two fields. We will pick up where we left off in this article and add buttons which can add and delete rows from the table. There are two forms attached to this article, one is the starting form which contains a simple table and the second is the completed result. The goal of these articles is to demonstrate how to create Lotus Forms XForms tables without the use of the Designer's Table Wizard. The table wizard is great for basic Lotus Forms tables, but if you need to write some more complex table logic, you may need more manual control over your table. If you're going to make your own tables manually, you're going to want to understand how XForms tables work and the goal of this article is to help you do that.
Step 1: Create a button
Step 2: Add the XForms insert action to the button
Step 3: Add the template row using the origin property
Step 4: Creating new rows with buttons inside the table
Step 5: The Delete Button
Step 6: Dealing with the Empty Table Problem
In XForms repeat items, the tables are driven by their data. In order to add another row to the table, you simply need to add another row to the XML data instance that is behind it. Therefore, both the add (XForms insert) and remove (XForms delete) actions deal directly with the repeating data in the XML instance for the table. It's worth noting that these actions can be used in situations outside of XForms tables, as a way to copy or remove data elements from a form.
To understand how to use the insert and delete actions, you need to understand a bit about how XForms work. For both of these XForms actions, the context of the button which contains them is very important. What this means is that if you use a relative XPath, you'll get a different result if your add row button is inside the XForms table than if it is outside, because when it is inside, its context is the current row of the table.
Note: these steps assume you have an XForms table on your page already. You can download the file named startingForm-TableExercise.xfdl from this article to use as a starting point.
XForms actions can live in multiple places and be triggered by several different events. With tables, typically they live in an XFDL button and are triggered by the user clicking on said button. So step one is to create a button that can house your xforms insert or delete action. First you should make sure you are in the Advanced Lotus Forms Designer perspective. To do this, click on the "Window" menu, then choose "Open Perspective" then choose "Advanced Lotus Forms Designer". Now that you're in the advanced perspective, you can access the Advanced Library section of the palette. Within the palette you will need to click on the Advanced Library bar to expand it. Now to create the button, select "Button (trigger)" from the palette and drag it on to your form canvas. Put the button on the canvas outside of your existing table.
Click once on the button in the canvas to select it, now look at your Designer's properties view. You should see the properties for the button. Expand the XForms section. In that section, you'll see an Actions section, expand that. You will see that there is an "action" already there. This is an empty container action that is ready for you to add new actions to. Click to expand it and you will see an "Actions" property with the word <empty> beside it. This is where you will add the XForms insert action.
Click on the word <empty> to get a dropdown list of actions. Select "insert" from the list, then click the + button to the right of it to add the insert action to the button. Now you should see all the available properties you can set for the XForms insert action. Here's a description of the purpose of each one:
|at||XPath (must resolve to integer)||This property expects an integer value, and that determines where in the nodeset you want to insert the new row. |
|position ||before or after ||Must contain before or after. This determines where the new row is placed in relation to the indexed row from the nodeset by the at property above. ||
|context ||XPath (single node)||(optional) Points to the parent node of the table which contains all the rows. This property is only necessary when inserting a new row into an empty table. If your table can never be empty, leave this value blank.|
|origin ||XPath (single node)||(optional) Should point to a single row node. This indicates a template row that you want to insert into the table. Note that this row node can (and should) exist outside the table structure. A best practice is to create a separate template node (or even a whole data instance) which holds all your template rows. If this origin property is left blank, then the insert operation will duplicate the last element in the nodeset provided below (this means that if the last row of the table has data, you get a duplicate of that data in a new row).|
|if||XPath ||(optional) Here you can provide an XPath which resolves to true or false. This allows you to control when the action fires or not. For example, you can use this option to stop the user from adding rows when the table has reached a certain size.|
|while ||XPath ||(optional) This property also takes an XPath which resolves to true or false. The XPath is tested, and if true, the action fires, then it re-evaluates the XPath, if it is still true, it fires again, and so on. This allows you to loop and fire the action multiple times. Be careful, because you can create an infinite loop, which will hang the viewer or your webform server session.|
|nodeset||XPath (selects zero, one or multiple nodes)||This is probably the most important property of the action. The nodeset determines the target where the insert will happen. The nodeset XPath should (in most cases) select all of the table's row elements. The insert action will put the new row in as a sibling of the indexed row (using the number provided in the "at" property) and the new row will go either before or after that sibling depending on your setting of the "position" property. If the nodeset XPath returns an empty nodeset (for example, there may be no rows to select in your table) then the action will fall back to the "context" property and try to insert the new row as a child of the element selected by the context.|
|bind||String (bind ID)||As an alternative to the nodeset property, you can refer to an existing XForms bind in your model using that bind's ID. In this case, the nodeset that is provided in that bind will be used as the nodeset for the insert operation. You should never provide both a bind and a nodeset in an action.|
|model||String (model ID)||(optional) This property is rarely used. If left blank, the first model in the document is selected for use. XForms allows for multiple models, each with their own ID. In 99% of cases, there is no reason to use multiple XForms models in Lotus Forms. Using a single XForms model is sufficient. Note that an XForms model is not to be confused with an XForms instance. Each model contains one or more XForms instances.|
Now let's begin to fill in the insert action, for the first example, we'll keep it simple and just make an insert action which duplicates the last row in the table and puts the new row at the end of the table. Let's start with the most important property, the nodeset. How do you determine what your nodeset should be? Simply put, it should be the same nodeset that is driving your table. To create a new row in your table, you insert the new node into the same nodeset that is driving the table. So to make sure you have the right nodeset, the best thing to do is to copy it directly from the table you're adding to. By doing so, you avoid making a typo in your reference -- bad XPath references can be very difficult to debug. In order to copy the reference from the table, follow these steps:
1. Click on the XForms table on your canvas (or you can select it from the outline view).
2. Look in the properties view under the XForms (repeat) category.
3. Copy the contents of the "nodeset" property. In this example form, it will be: instance('formData')/table/row
Warning! If the table is nested inside a pane or another table, it may not be using an absolute reference. If the table's nodeset is absolute (it will start with "instance('instanceID')"). If the table's nodeset is not an absolute XPath reference, you will need to either make sure that your insert button is in the same container as your XForms table (in which case it can use the same relative XPath in the table's nodeset), or that you get the absolute XPath that is used for the table instead (you can do this by finding the proper row node in the Instance view of the Designer and copying a reference to that element).
4. Click on the add button.
5. In the properties view, expand your xforms:insert action and paste the reference into the nodeset property of the action. When you're done, you should see this:
Next, we'll add the "at" property to indicate where we want to put the new row in the nodeset. Since we want to put it last, at the end of the table, we need to know the number of the last row in the table. There is actually a handy XPath function for this, appropriately called "last()". The last() method will always return the size of the context node set. You need to know that the context for the evaluation of the XPath you provide in "at" is always the nodeset selected by the XPath in the nodeset property of the xforms:insert action.
Finally, you need to specify the position, selecting either "before" or "after". Choose "after" in this example so that the new rows are added after the last row in the table. Your insert action should now look like this:
Run your form with the preview tab to test out the insert button. It should add a row to the table when you click it. If you used the starter form attached to this article, you'll see that the sample data in the last row is duplicated each time you click the add row button.
Let's improve our table so that the row that is added is blank instead of duplicating existing data. Note that we could do this by adding additional actions to the add row button that erased the data in the new row every time, but a better practice is to use an empty template row as the duplication source and use it with the origin property.
In this sample form, we've already created a separate "templates" XForms instance. To do this yourself, you would create a new instance and create an instance structure with a single row from your table in it with no data in the row. Remember that the XML structure and element names must match the existing rows.
1. To get a reference to the template row for use in the origin property, use the Instance view.
2. Expand the templates instance and find the row element. Right click on it and select "copy reference".
3. Click on the existing add row button on your form.
4. In the properties view, expand the XForms section and drill down until you find the insert action you added earlier.
5. Paste the copied reference into the origin property. It should look like this when you are done:
Now test your form using the preview pane. You should see now that adding new rows creates blank rows in the table instead of duplicating data.
Now we'll try something a little different to illustrate how nodeset context is important. We're going to move the button inside the table. In doing so, the button will then inherit the context of the table. When the button is outside the table, we only get one add row button and all its XPath references must be absolute. When the button is moved inside the table, the button gets duplicated with the rows in the table, and each copy of the button inherits the context of that row. We can use this to our advantage and alter the button's insert action so that each button in each row will add a new row right below it. This gives the user more control over their table data. Here's how.
1. Drag the button into the table and position it to the right of the rightmost field in the table.
2. Select the button and in the properties view, drill down to the insert action.
3. Now you'll need to change the nodeset property from the absolute reference of "instance('formData')/table/row" to the relative reference of ".". Yes, just a period. In XPath this means "the current node". You may be asking yourself why this should produce any different result that the full absolute reference. The answer is that the context for any one of those buttons is the row that button is in, for example, the for the button in the third row, its context is just the third row element, so we get a nodeset of just that third row element. When we use the absolute reference instead, we get a nodeset of ALL the rows in the table. You should consider your "at" property now that the nodeset has changed. When the nodeset was all the rows in the table, the position where the new row was inserted really mattered. Now, since the nodeset has changed to just one single row, using a value of "1" or "last()" in the "at" property will have the exact same result.
4. Preview the form, you should see that you get one add row button per row now, and clicking the button adds a new row right below the row you clicked on.
Now we'll round out the table with a delete button. Creating a delete row button is very similar to the add row button, and you can also create it inside or outside the table. Typically, more so than with the add button, the delete button should be in the table, one with each row, because it doesn't make sense to limit the user to only deleting the last row in the table one by one.
1. Add another button to the form from the palette. As with the add row button, use the "Button (Trigger)" from the palette. Drag the button into the table and place it to the right of the Add Row button.
2. In the properties view for the button, add a delete action to the action container as you did for the insert button.
3. Now configure the properties of the delete action. You'll notice the set of properties for the delete action is quite similar to the properties of the insert action. It's actually quite a bit simpler since we don't have to concern ourselves with duplicating any data and where to put that data. All we really need to do is identify a node and delete it and all its children. Fundamentally, the delete action requires a nodeset and the "at" property to pick one of the nodes from the nodeset, but in most cases, we can just make our nodeset be a single node and then the "at" property is irrelevant (you can just set it to "1"). In this case, since the delete button is inside the table, we can just set the nodeset to be "." and it will inherit the current row from the table, thereby deleting the current row and itself all at once. Fill in your delete action properties like so:
Now test your form in the preview pane. You should see a delete button on each row of your table, and clicking the button for any given row will delete that row.
Now we have a problem, we can delete every row in the table and be left with an empty table. With the add row button inside the table, after we delete the last row, we have no way to add new rows to the table again. There are two solutions, either put the add row button outside the table again (adding the context property setting so that new rows can be added to an empty nodeset), or use an "if" property on the delete button which prohibits the use of the delete action when there is only one table row remaining. It's up to you how you do this, in the final result form that is attached to this article, you'll see an example of both solutions.