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:
- Adding a new
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.
- Removing a
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:
- Relying on the session for critical information is not great: sessions can get invalidated (users logout, for example) and they are not scaling-friendly.
- Saving objects for the sake of not having to carry them in the view is a bad, fragile idea - it can easily break (session gets invalidated, user closes the browser, there's a deployment and sessions are not persisted) and requires clean up. It can be done, but the cost is too high in my opinion.
I think this calls for a better client instead of relying on server tricks. The changes you described don't make it very different.
- Having the index as a property of
B
makes think easier than dealing with a SortedSet
/List
:
- when showing
A
, a.b.sort { it.index }
needs to be added to preserve the order.
- when rendering
B
, a hidden input for the index needs to be added.
- When drag'n'drop' or deleting happens, a Javascript function that recalculates the indexes is needed.
- When binding the data, nothing changes, since the index is just a property.
- Having inheritance in
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?
- when rendering a new
B
, the "type" (domainClassName
) needs to be passed
- when rendering
B
, if it has no id
, a hidden input for the class name needs to be passed
- when saving
A
, 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
.
- When the user adds a new
B
, confirmed is set to false.
- When the user saves an
A
, all the belonging B
's that have not being deleted get confirmed
set to true
, the deleted ones are well, deleted :).
- When showing
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 :-).