The article was written during development. Therefore final solution differs from the code presented in the beginning of this post. If You want to see how working code looks like just check my Github.
It is quite common situation – we want two domain classes to share one form. In addition there is also oneToMany relation between them. And to make things a little bit harder we need to keep order of many side of the relationship – despite delete or create operations. With simple drag&drop functionality. Of course in Grails. Are You ready? Let’s go.
I started with solution described by ObjectPartners, but it got one big problem. It was using additional jQuery plugin, and therefore was doing some kind of ‘under the hood’ job, which was not suitable for my needs. So I have switched to plain jQuery and its plugins mainly due to main information source about this topic. My solution is fairly similar to described in omarello post, but if You do not need other form elements that inputs in your list and do not even think about sorting and ordering them, solution provided by by ObjectPartners will suffice.
Grails way of oneToMany
Before coding there is an important issue raised in comments section. Imports required for code snippets to work include:
import org.codehaus.groovy.grails.plugins.springsecurity.GrailsUserDetailsService
import org.springframework.security.core.authority.GrantedAuthorityImpl
import org.springframework.security.core.userdetails.UserDetails
We should start with two simple domain classes:
package com.wordpress.chlebik
class Article {
String title
ArticleType type
boolean deleted
static transients = [ 'deleted' ]
static constraints = {
title(size: 3..12, nullabe: false)
}
String toString() {
title
}
}
enum ArticleType {
NEWS,
STORY
}
package com.wordpress.chlebik
class Newspaper {
String name
List<Article> articles = []
static hasMany = [articles: Article]
static mapping = {
articles cascade:"all-delete-orphan"
}
static constraints = {
name(nullable: false, blank: false, size: 3..16)
}
String toString() {
name
}
}
They are simple but provide several usefull features – like list of types in enum and constraints. They are important in this example, but for now we should just focus on simple relation. Newspaper consists of Articles – you should pay attention to the fact, that I have used not only belongsTo mapping, but directly declared that there is a List of Articles. Default Grails’ behaviour with oneToMany relations is to use Set. We want to maintain order fo our Articles so I used List.
If You run application with default data source settings – Grails will automatically create tables in memory (with h2 db engine). Just visit http://localhost:8080/YOURAPPNAME/dbconsole and see by yourself what is going on. We have two simple tables for domain classes and one join table – NEWSPAPER_ARTICLE. There are two columns with foreign keys and what is more important – there is articles_idx column, which is needed for keeping order of articles. And all that thanks to Grails’ db creation. Awesome.
We got model created, now it is time for web layer. Within Grails’ console issue a command:
generate-all com.wordpress.chlebik.Newspaper
and in a moment we have generated controller and views. Unfortunately they do not provide possibility to create new articles within newspaper but it gives us a solid code-base to proceed. First, we should create a newspaper with articles in it. I will not use form generated, as it lacks functionality we need. Nothing as simple as issuing this code on controller or bootstrap:
Newspaper n = new Newspaper(name: 'ChlebikZine')
Article a1 = new Article(title: 'Title1', type: ArticleType.NEWS)
Article a2 = new Article(title: 'Title2', type: ArticleType.NEWS)
Article a3 = new Article(title: 'Title3', type: ArticleType.STORY)
n.articles.add(a1)
n.articles.add(a2)
n.articles.add(a3)
n.save()
Edit and update existing domain class
After running the application we can visit http://localhost:8080/YOURAPPNAME/newspaper/show/1 to see how our newspaper is rendered (I’ve changed page source generated by Grails, eg. removed links to articles edition).
It is show action and it looks fine. To now we can switch to editing. I am interested in list with editable fields. Right now I also want to have delete link to remove existing records (we do not add new ones so far). So our _form.gsp should look like this.
<%@ page import="com.wordpress.chlebik.Newspaper" %>
<script type="text/javascript">
jQuery(document).ready( function() {
$(document).on("click", ".deleteArticle",function(event) {
var tableBody = $('#articlesListTableBody');
var rowToDelete = $(this).closest('tr');
var rowId = rowToDelete.attr('rowId');
// This is for removing only already existing rows in DB.
if( !$(rowToDelete).attr('newRow') ) {
$(tableBody).append("<input type='hidden' name='articles[" + rowId + "].deleted' value='true' />");
}
$(rowToDelete).remove();
return false;
});
$('.addNewArticle').click( function() {
});
} );
</script>
<div class="fieldcontain ${hasErrors(bean: newspaperInstance, field: 'name', 'error')} required">
<label for="name">
<g:message code="newspaper.name.label" default="Name" />
<span class="required-indicator">*</span>
</label>
<g:textField name="name" maxlength="16" required="" value="${newspaperInstance?.name}"/></div>
<div class="fieldcontain ${hasErrors(bean: newspaperInstance, field: 'articles', 'error')} ">
<label for="articles">
<g:message code="newspaper.articles.label" default="Articles" />
</label>
<table id="articlesList">
<thead>
<tr>
<th>Title</th>
<th>Type</th>
<th>Delete</th>
</tr>
</thead>
<tbody id="articlesListTableBody">
<g:each in="${newspaperInstance.articles}" var="article" status="i">
<tr rowId="${i}">
<td>
<g:textField required="" name="articles[$i].title" value="${article.title}"/>
<g:hiddenField name="articles[$i].id" id="articles[$i].id" value="${article.id}"/></td>
<td>
<g:select value="${article.type}" name="articles[${i}].type" from="${com.wordpress.chlebik.ArticleType?.values()}" keys="${com.wordpress.chlebik.ArticleType.values()*.name()}" /></td>
<td>
<a href="#" class="deleteArticle">Delete article</a></td>
</tr>
</g:each></tbody>
</table>
</div>
It is quite simple. Now here is a catch.
In Grails versions before 2.2.x UPDATE method was working differently than shown below. Solution given by ObjectPartners is working then but in newer versions it is not! Check out StackOverflow question. My solution was tested in Grails 2.4.3 with Groovy 2.3.6 and is mainly based on the solution given by Omarello. But it is just partly true. Keep reading to see what I’ve changed to make it work.
Therefore update method of the controller should like this:
@Transactional
def update(Newspaper newspaperInstance) {
if (newspaperInstance == null) {
notFound()
return
}
if (newspaperInstance.hasErrors()) {
respond newspaperInstance.errors, view:'edit'
return
}
newspaperInstance.articles.eachWithIndex{ v,i ->
if( params['articles[' + i + '].deleted'] == 'true' ) {
v.deleted = true
}
}
newspaperInstance.articles.removeAll{ it.deleted }
newspaperInstance.save flush:true
request.withFormat {
form multipartForm {
flash.message = message(code: 'default.updated.message', args: [message(code: 'Newspaper.label', default: 'Newspaper'), newspaperInstance.id])
redirect newspaperInstance
}
'*'{ respond newspaperInstance, [status: OK] }
}
}
If you know better solution please let me know – it seems that wiring between form data/params is not transffering deleted property into created Newspaper entity. I’ve tried using:
newspaper.properties = params
With params filled with proper-named attributes for articles but it is not working! So I had to use additional iteration over all articles to set their deleted property. Again – if there is something I am doing wrong please let me know.
Now we should think about adding new articles to our newspaper. As You have seen it is quite simple – we just need to add inputs/selects to our table (in a view layer) with proper name attribute and that is all. Grails should do all the hard work. So after table we add simple link:
<a href="#" class="addNewArticle">Add new article</a>
And of course do not forget about Javascript.
$('.addNewArticle').click( function() {
var allArticles = $("#articlesListTableBody tr[rowId]");
var rowId = 0;
// This operation is performed to allow safe-delete of newly created (not saved in DB) articles
if( allArticles.length ) {
var maxId = 0;
allArticles.each(function() {
maxId = Math.max(maxId, parseInt($(this).attr('rowId')));
});
rowId = maxId+1;
}
$("#articlesListTableBody").append( '<tr newRow="true" rowId="' + rowId + '">' +
'<td><input type="text" required="" name="articles[' + rowId + '].title" value=""/></td>' +
'<td><select name="articles[' + rowId + '].type" >' +
'<g:each in="${com.wordpress.chlebik.ArticleType?.values()}" var="type">
<option value="${type}">${type}</option></g:each>
</select></td>
' +
'
<td><a href="#" class="deleteArticle">Delete article</a></td>
</tr>
');
return false;
});
Nothing new here. We just add another row in our table. Pay attention to the fact, that there is newRow attribute in newly created row. Now it is clear why there was check for that attribute in delete function – if new article was not stored in DB, removal of it is not needed to be presented with deleted property set. You can play around with above solution – adding and deleting articles was never so easy. There is one thing to add – I resigned from cloning existing rows like it was presented in Omorello post. It is possbile when there are for sure rows in articles list and in my way – we can start with empty list so there is nothing to clone. Appending pure strings is maybe more low-level, but gives developer total control (which was compromised by using writetable-plugin).
Validate me
So far we did not checked two things – creation of totally new newspaper (that will come in a moment), ad validation (in facts it is about re-rendering of the view). Let’s have a look.
In generated views there is required attribute in inputs which are responsible for rendering domain class properties that have nullable : false in constraints. Newspaper name is one of them. Let’s see what will happen if we remove that attribute in GSP and submit empty newspaper name.
Apparently everything is all-right. Added article was successfully persisted in the DB. That is because our newspaper is separated from articles – constraint violation on newspaper do not stop persisting new article (because it is proper entity). Now let’s try to remove required attribute from article title and try to submit new article without it – sorry. We receive error message.
Do not ask me why and what does it mean. fortunately I know what helps – removing @Transactional annotation on our update method in controller!. If you remove it, form will re-render with proper error message. But try to submit form another time with new article empty title. It will give us error with finding deleted property in our code.
newspaperInstance.articles.removeAll{ it.deleted }
Somehow Grails managed to create new article which violates constraint (empty title) and is trying to save it! The cure for this is adding if statement in GSP that is listing all articles.
<g:each in="${newspaperInstance.articles}" var="article" status="i">
<tr rowId="${i}">
<td>
<g:textField required="" name="articles[$i].title" value="${article.title}"/>
<g:if test="${article.id}">
<g:hiddenField name="articles[$i].id" id="articles[$i].id" value="${article.id}"/>
</g:if></td>
<td>
<g:select value="${article.type}" name="articles[${i}].type" from="${com.wordpress.chlebik.ArticleType?.values()}" keys="${com.wordpress.chlebik.ArticleType.values()*.name()}" /></td>
<td>
<a href="#" class="deleteArticle">Delete article</a></td>
</tr>
</g:each>
This is the second time I do not know why Grails behaves like this – I understand that when there is no id attribute Grails will treat created domain class as new instance to persist. But how can it try to persist an entity which fails validation! Just to remember, generated update method has at the beginning:
if (newspaperInstance == null) {
notFound()
return
}
if (newspaperInstance.hasErrors()) {
respond newspaperInstance.errors, view:'edit'
return
}
Why this works for the first empty-title submit and fails at the second – I have no idea. Fortunately adding show-id logic in GSP helped.
But I want new newspaper
We started this topic from existing entity and editing it. It was easier that way to create clean code and show what is essential in oneToMany in one form. Right now we are going to create new newspaper and add some articles to it. There is already generated form and controller action. The truth is that – it works 馃槈 But only if we do not start to fool around validation again. To make long things short – there is a problem with treating newly added articles as new ones, not stored in DB (therefore they do not have id). _form.gsp must be modified in a section reponsobile for rendering article list (mainly safe-operator logic).
<table id="articlesList">
<thead>
<tr>
<th>Title</th>
<th>Type</th>
<th>Delete</th>
</tr>
</thead>
<tbody id="articlesListTableBody">
<g:each in="${newspaperInstance.articles}" var="article" status="i">
<tr <g:if test="${!article?.id}">newRow="true"</g:if> rowId="${i}">
<td>
<g:textField required="" name="articles[$i].title" value="${article?.title}"/>
<g:if test="${article?.id}">
<g:hiddenField name="articles[$i].id" id="articles[$i].id" value="${article?.id}"/>
</g:if></td>
<td>
<g:select value="${article?.type}" name="articles[${i}].type" from="${com.wordpress.chlebik.ArticleType?.values()}" keys="${com.wordpress.chlebik.ArticleType.values()*.name()}" /></td>
<td>
<a href="#" class="deleteArticle">Delete article</a></td>
</tr>
</g:each></tbody>
</table>
Now we can do whatever we want with creation of new entity – try to break validation, add and delete articles, try to break articles’ validation, whatever You want. It just works 馃槈
Gimme more – I want to sort things out
Generally I did not expect sorting to work so easily and quickly. Of course You need to download/link jQeury UI with sortable. After that You must change _form.gsp to this:
<%@ page import="com.wordpress.chlebik.Newspaper" %>
<script type="text/javascript">
jQuery(document).ready( function() {
$(document).on("click", ".deleteArticle",function(event) {
var tableBody = $('#articlesListTableBody');
var rowToDelete = $(this).closest('tr');
// This is for removing only already existing rows in DB.
if( !$(rowToDelete).attr('newRow') ) {
var rowId = $(rowToDelete).attr('rowId')
var articleId = $("input[name='articles[" + rowId + "].id']").val();
$(tableBody).append("<input type='hidden' name='articles[" + articleId + "].deleted' value='true' />");
}
$(rowToDelete).remove();
return false;
});
$('.addNewArticle').click( function() {
var allArticles = $("#articlesListTableBody tr[rowId]");
var rowId = 0;
// This operation is performed to allow safe-delete of newly created (not saved in DB) articles
if( allArticles.length ) {
var maxId = 0;
allArticles.each(function() {
maxId = Math.max(maxId, parseInt($(this).attr('rowId')));
});
rowId = maxId+1;
}
$("#articlesListTableBody").append( '<tr newRow="true" rowId="' + rowId + '">' +
'<td><input required="true" type="text" name="articles[' + rowId + '].title" value=""/></td>' +
'<td><select name="articles[' + rowId + '].type" >' +
'<g:each in="${com.wordpress.chlebik.ArticleType?.values()}" var="type">
<option value="${type}">${type}</option>
</g:each></select></td>' +
'<td class="moveRow"><a href="#" class="deleteArticle">Delete article</a></td>
<td class="moveRow">Move</td></tr>');
return false;
});
$('#articlesListTableBody').sortable({
stop: function (event, ui) {
updateNames($(this))
},
handle: '.moveRow',
});
function updateNames($tbody) {
$tbody.find('tr').each(function (idx) {
var $inp = $(this).find('input,select,textarea');
$(this).attr('rowId', idx);
$inp.each(function () {
this.name = this.name.replace(/(\[\d\])/, '[' + idx + ']');
})
});
}
} );
</script>
<div class="fieldcontain ${hasErrors(bean: newspaperInstance, field: 'name', 'error')} required">
<label for="name">
<g:message code="newspaper.name.label" default="Name" />
<span class="required-indicator">*</span>
</label>
<g:textField name="name" required="" maxlength="16" value="${newspaperInstance?.name}"/></div>
<div class="fieldcontain ${hasErrors(bean: newspaperInstance, field: 'articles', 'error')} ">
<label for="articles">
<g:message code="newspaper.articles.label" default="Articles" />
</label>
<table id="articlesList">
<thead>
<tr>
<th>Title</th>
<th>Type</th>
<th>Delete</th>
<th>Move</th>
</tr>
</thead>
<tbody id="articlesListTableBody">
<g:each in="${newspaperInstance.articles}" var="article" status="i">
<tr <g:if test="${!article?.id}">newRow="true"</g:if> rowId="${i}">
<td>
<g:textField required="" name="articles[$i].title" value="${article?.title}"/>
<g:if test="${article?.id}">
<g:hiddenField id="" name="articles[$i].id" value="${article?.id}"/>
</g:if></td>
<td>
<g:select value="${article?.type}" name="articles[${i}].type" from="${com.wordpress.chlebik.ArticleType?.values()}" keys="${com.wordpress.chlebik.ArticleType.values()*.name()}" /></td>
<td>
<a href="#" class="deleteArticle">Delete article</a></td>
<td class="moveRow">
Move</td>
</tr>
</g:each></tbody>
</table>
<a href="#" class="addNewArticle">Add new article</a></div>
JavaScript code used in this example comes from StackOverflow question – it works just out-of-the-box. But there is a catch – only with already persisted entities. So create new newspaper, add several new articles, save them. After that edit newspaper switch order and save it again. It works like a charm.
The problems begin when removal of rows or changing order of not yet persisted rows – I receieved many different errors and logical errors. It took me about two hours – after that I was not sure what was the reason that it started working 馃槈 But it does – I definetly added refreshing input-indexes after row removal in Javascript (same action that is triggered when sorting ends). I’ve removed delete property completly – and it still works. Magic.
So here is view and controller.
<%@ page import="com.wordpress.chlebik.Newspaper" %>
<script type="text/javascript">
jQuery(document).ready( function() {
$('#articlesListTableBody').sortable({
stop: function (event, ui) {
updateNames($(this))
},
handle: '.moveRow',
});
function updateNames($tbody) {
$tbody.find('tr').each(function (idx) {
var $inp = $(this).find('input,select,textarea');
$(this).attr('rowId', idx);
$inp.each(function () {
this.name = this.name.replace(/(\[\d\])/, '[' + idx + ']');
})
});
}
$(document).on("click", ".deleteArticle",function(event) {
var tableBody = $('#articlesListTableBody');
var rowToDelete = $(this).closest('tr');
$(rowToDelete).remove();
updateNames(tableBody);
return false;
});
$('.addNewArticle').click( function() {
var allArticles = $("#articlesListTableBody tr[rowId]");
var rowId = 0;
// This operation is performed to allow safe-delete of newly created (not saved in DB) articles
if( allArticles.length ) {
var maxId = 0;
allArticles.each(function() {
maxId = Math.max(maxId, parseInt($(this).attr('rowId')));
});
rowId = maxId+1;
}
$("#articlesListTableBody").append( '<tr newRow="true" rowId="' + rowId + '">' +
'<td><input required="true" type="text" name="articles[' + rowId + '].title" value=""/></td>' +
'<td><select name="articles[' + rowId + '].type" >' +
'<g:each in="${com.wordpress.chlebik.ArticleType?.values()}" var="type">
<option value="${type}">${type}</option></g:each></select></td>' +
'<td class="moveRow"><a href="#" class="deleteArticle">Delete article</a></td><td class="moveRow">Move</td></tr>');
return false;
});
} );
</script>
<div class="fieldcontain ${hasErrors(bean: newspaperInstance, field: 'name', 'error')} required">
<label for="name">
<g:message code="newspaper.name.label" default="Name" />
<span class="required-indicator">*</span>
</label>
<g:textField name="name" required="" maxlength="16" value="${newspaperInstance?.name}"/></div>
<div class="fieldcontain ${hasErrors(bean: newspaperInstance, field: 'articles', 'error')} ">
<label for="articles">
<g:message code="newspaper.articles.label" default="Articles" />
</label>
<table id="articlesList">
<thead>
<tr>
<th>Title</th>
<th>Type</th>
<th>Delete</th>
<th>Move</th>
</tr>
</thead>
<tbody id="articlesListTableBody">
<g:each in="${newspaperInstance.articles}" var="article" status="i">
<tr <g:if test="${!article?.id}">newRow="true"</g:if> rowId="${i}">
<td>
<g:textField id="" required="" name="articles[$i].title" value="${article?.title}"/>
<g:if test="${article?.id}">
<g:hiddenField id="" name="articles[$i].id" value="${article?.id}"/>
</g:if></td>
<td>
<g:select value="${article?.type}" name="articles[${i}].type" from="${com.wordpress.chlebik.ArticleType?.values()}" keys="${com.wordpress.chlebik.ArticleType.values()*.name()}" /></td>
<td>
<a href="#" class="deleteArticle">Delete article</a>
</td>
<td class="moveRow">Move</td>
</tr>
</g:each></tbody>
</table>
<a href="#" class="addNewArticle">Add new article</a></div>
def update(Newspaper newspaperInstance) {
if (newspaperInstance == null) {
notFound()
return
}
if (newspaperInstance.hasErrors()) {
respond newspaperInstance.errors, view:'edit'
return
}
newspaperInstance.articles.clear()
newspaperInstance.properties = params
newspaperInstance.save flush:true
request.withFormat {
form multipartForm {
flash.message = message(code: 'default.updated.message', args: [message(code: 'Newspaper.label', default: 'Newspaper'), newspaperInstance.id])
redirect newspaperInstance
}
'*'{ respond newspaperInstance, [status: OK] }
}
}
Almost finished
Looking back at all the trouble I had with figuring out the proper solution made me wonder – why do not use AJAX? This is fine idea but I wanted to deal with pure Grails’ form handling. Full source (with final solution) can be found on my Github. As I mentioned before – I have used Grails 2.4.3 with Groovy 2.3.6.