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
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 B
s 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:
B
would trigger an Ajax call to retrieve this template for a new B, prefix b
(the name of A
's property) and an index that corresponds to the lenght of the list.B
would simple remove the HTML fragment correspoding to the template, and recalculate the indexes.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.
B
makes think easier than dealing with a SortedSet
/List
:
A
, a.b.sort { it.index }
needs to be added to preserve the order.B
, a hidden input for the index needs to be added. B
really requires to have the domain class as a hidden input in the view (or use some Javascript for tracking that information, but I don't see the benefit). I don't see why is this bad. You are using inheritance as sort of "Type of B". If instead of inheritance you had a property in B
called type
, you'd use an input for it, right?
B
, the "type" (domainClassName
) needs to be passedB
, if it has no id
, a hidden input for the class name needs to be passedA
, the new B
's are created using the specific domain class, otherwise nothing changes.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
.
B
, confirmed is set to false.A
, all the belonging B
's that have not being deleted get confirmed
set to true
, the deleted ones are well, deleted :).A
, only the confirmed B
's are displayed.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 B
s based on some timeouts, but it's tricky - as the whole idea of saving non confirmed data is :-).