springspring-webflowspring-webflow-2

Spring Webflow - deleting an item from a list?


I'm using Webflow 2.3.2 in an application, and on one step the user can add/delete from a list in the bound model object (they simply return to the current step after the modification). For example my object might look like this:

public class MyInfo implements Serializable {
    List<String> myList = new ArrayList<String>();
}

Doing the "add" in webflow is no problem because I simply stick the new object on the end of the list, but for the "delete" I need to identify the element to delete. What I'm doing now is using the "currentEvent" predefined EL object and grabbing the raw event "value" which I've populated with the ID of the record to delete. I'm wondering if there's a more elegant way to do this, because this seems like going the long way around. Can anyone suggest a better way of doing this? Here's an illustration of what I'm doing now:

My JSP file (note the "delete" button):

<c:forEach items="${myInfo.myList}" var="listItem" varStatus="listItemStatus">
    <c:set var="v" value="${listItemStatus.index}"/>
    <div><form:input id="listItemValue_${v}" path="myInfo.myList[${v}]"/></div>
    <div><button id="deleteItem_${v}" name="_eventId_deleteItem" type="submit" value="${v}">Delete This Item</button></div>
</c:forEach>

My "flow.xml" file:

<transition on="deleteItem"  bind="false" validate="false">
    <evaluate expression="flowService.deleteItem(flowScope.myInfo, currentEvent.attributes)"  result="flowScope.myInfo"  />
</transition>

My event handler:

public MyInfo deleteAccount(MyInfo myInfo, LocalAttributeMap currentEvent) {
    myInfo.getMyList().remove(Integer.valueOf((String)(currentEvent.asMap().get("_eventId_deleteItem"))).intValue());
    return myInfo;
}

Solution

  • It seems like the main issue is how to submit both the _eventId and account index using a single <button type='submit'/> element?

    Here is a great question & answers about exactly this issue: How do you overcome the html form nesting limitation?. Using a hidden input with the account index doesn't help because the submit button would submit all of the hidden inputs anyway.

    Of course, you could use an anchor instead of a submit button as long as you didn't want to submit anything else, e.g.

    <a href="${flowExecutionUrl}&_eventId=deleteItem&index=${v}">Delete This Item<a> and

    <transition on="deleteItem"  bind="false" validate="false">
        <evaluate expression="flowService.deleteItem(flowScope.myInfo, requestParameters.index)" result="flowScope.myInfo"/>
    </transition>
    

    but semantically, a "delete" operation shouldn't be an HTTP GET request. I tend to use an anchor anyway, because it seems like the least hacky alternative. But, after reading through the linked question, I see that there is an HTML5 alternative - the "formaction" attribute. In this case, the JSP would look something like this:

    <form:form action="${flowExecutionUrl}" method="post" commandName="backingObject">
    
    <c:forEach items="${myInfo.myList}" var="listItem" varStatus="status">
    
        <form:input path="myInfo.myList[${status.index}]"/>
    
        <button type="submit" name="index" value="${status.index}" 
               formaction="${flowExecutionUrl}&_eventId=deleteItem">
            Delete This Item
        </button>
    
    </c:forEach>
    
    </form:form>
    

    Basically, the selected button overrides the enclosing form's 'action' attribute, to include the _eventId parameter.

    You'd need to consider the browser support, but maybe you can provide a javascript polyfill to support older browsers.


    Side note

    Since selecting an item from a list (for the purposes of deleting it, displaying it, whatever) is a very common use-case, I like to use a custom list class which allows you to bind the index to the list itself:

    public class SelectionList<T> extends ArrayList<T> {
    
        // the index of the selected item within the list
        private Integer index;
    
        public SelectionList() {
        }
    
        public SelectionList(Collection<? extends T> c) {
            super(c);
        }
    
        // this is used for binding to/from the view layer, and shouldn't be called by application code
        public Integer getIndex() {
            return index;
        }
    
        // this is used for binding to/from the view layer, and shouldn't be called by application code
        public void setIndex(Integer index) {
            this.index = index;
        }
    
        // use this to retrieve the selected item from the list.
        public T getSelectedItem() {
            if (index == null || index >= super.size() || index < 0) {
                return null;
            }
            return super.get(index);
        }
    }
    

    Then, you could bind the selected index directly to the list and just call list.getSelectedItem() after binding has occurred.