javascriptajaxgrailswindow.onunload

Handling unsaved state of objects in grails


I have a problem that probably is rather a design problem than a technical one.

Assuming I have these classes:

class A {
    def someInfo
    static hasMany = [b: B]
}

class B {
    A a
    def info
    def moreInfo
    ...
    def aLotMoreInfo
}

Now let's say the user is on a page where he can edit the b's of an A and he can add new b's. But the user needs to save his changes to A, otherwise everything will be discarded.

My current approach is creating the additional b's, render them via AJAX and save their ID's in a session variable so I can delete the b's that are "unsaved".

This works quite well but for one common use case: The user refreshes the page.

I use the window.onunload-event to inform the user that he is going to lose his unsaved changes, and make an AJAX call to a delete function within it to delete the b's from the session-variable. Unfortunately the index function of the A-controller is called before I have deleted the b's. This means, the "unsaved" b's are shown and shortly afterthat they'll be deleted which would force me to make a refresh or wait for the b's to be deleted somehow.

Maybe the way I try to accomplish it is wrong anyway - in which case I'd be happy for any suggestions.

So the question is: How to keep an eye on new objects that possibly could be discarded without the need to store every information of it in hidden fields to create them on the save-function?

Update:

I should have mentioned it before but I thought it is not that important.

B is an abstract class which is extended by lots of classes such as the following example:

class childOfB extends B {
    def usefulExtraInfo
}

class anotherChildOfB extends B {
    def anotherUsefulExtraInfo
}

Beside that B has an integer field that represents the position within the Set of b's in A. I know that I could use a SortedSet for that but for some specific reasons it has to be a separate field. I mention this because the view renders each of it as an element of a sortable list which could be reordered by drag&drop.

Use case: The user adds a few childOfB, anotherChildOfB and reorders them as he needs. How would I track the type of them without storing the type in the view, which would be bad practice as well, I think?

Regards


Solution

  • the user needs to save his changes to A, otherwise everything will be discarded

    It sounds to me that you are eagerly creating the B's when there's no need to - you only want to create them when the user confirm the whole operation by saving A.

    a page where he can edit the b's of an A and he can add new b's

    It also looks like you have a single page where all the Bs are displayed for edit, so there's no real need to keep hidden fields all over the place.

    What I would do then is keep all the current changes in the view, using normal form inputs, and invoke a single, transactional operation that saves A and creates/modifies/removes the B's according to the params.

    Depending on how your application looks like, you can do this in several ways.

    One that I've used in the past is to have a template (let's say editB) that receives a B, an index and a prefix and display the corresponding inputs for that given B with the names prefixed by ${property}. (i.e it render a given B in edit mode).

    The edit view for A would then render editB for all the B's it has, and:

    Then, on saving A, the controller would inspect what is in params.list('b') and create, update and remove accordingly.

    Generally, it would be something like:

    Template /templates/_editB.gsp

    <g:if test="${instance.id}">
        <input type="hidden" name="${prefix}.id" value="${instance.id}" />
    </g:if>
    <g:else>
        <input type="hidden" name="${prefix}.domainClassName" value=${instance.domainClass.clazz.name}" />
    </g:else>
    <input type="hidden" name="${prefix}.index" value=${instance.index}" />
    <input type="..." name="${prefix}.info" value="${instance.info}" />
    

    Edit view for A

    <g:each var="b" in="${a.b.sort { it.index }}">
          <g:render template="/templates/editB" model="${[instance: b, prefix: 'b']}" />
       <button onClick="deleteTheBJustUpThereAndTriggerIndexRecalculation()">Remove</button>
    </g:each>
    <button onClick="addNewBByInvokingAController#renderNewB(calculateMaxIndex())">Remove</button>
    

    AController:

    class AController {
    
        private B getBInstance(String domainClassName, Map params) {
            grailsApplication
                .getDomainClass(domainClassName)
                .clazz.newInstance(params)
        }
    
        def renderNewB(Integer index, String domainClassName) {
            render template: '/templates/editB', model: [
                instance: getBInstance(domainClassName, [index: index]),
                prefix: 'b'
            ]
        }
    
        def save(Long id) {
            A a = a.get(id)
            bindData(a, params, [exclude: ['b']]) // We manually bind b
            List bsToBind = params.list('b')
            List<B> removedBs = a.b.findAll { !(it.id in bsToBind*.id) }
            List newBsToBind = bsToBind.findAll { !it.id }
            A.withTransaction { // Or move it to service
                removedBs.each { // Remove the B's not present in params
                    a.removeFromB(it)
                    it.delete()
                }
                bsToBind.each { bParams ->
                    if (bParams.id) { // Just bind data for already existing B's 
                        B b = a.b.find { it.id == bParams.id }
                        bindData(b, bParams, [exclude: 'id', 'domainClassName'])
                    }
                    else { // New B's are also added to a
                        B newB = getBInstance(bParams.remove('domainClassName'), bParams)
                        a.addToB(b)
                    }
                }
                a.save(failOnError:true)
            }
        }
    }
    

    The Javascript functions for invoking the renderNewB, for removing the HTML fragments for existing B's, and for handling the indexes are missing but I hope the idea is clear :).

    Update

    I'm assuming that:

    I think this calls for a better client instead of relying on server tricks. The changes you described don't make it very different.

    I've updated the code to reflect this changes.

    What if I really want to save the objects upfront?

    If you are really convinced this is the right approach, I would still try to avoid the session and add a new property to B called confirmed.

    Even if the user closes the browser or the sessions gets invalidated, the non confirmed B's are never displayed to the user, and will be eventually deleted when A is saved again. You can also add a Quartz job that periodically cleans the unconfirmed Bs based on some timeouts, but it's tricky - as the whole idea of saving non confirmed data is :-).