考虑下面的需求:在HR系统中,需要录入员工的家庭成员信息,而家庭成员分为配偶、子女、其他三大类,配偶需要填写较多的信息,而子女、其他类只需要填写较少的信息,如下图所示。

将家庭成员设计为一个Domain类,使用 relationship(关系)来区分其类别,Domain类如下所示:
/**
* 员工
*/
class Employee {
String name
/** 家庭成员 */
static hasMany = [familyMembers: FamilyMember]
}
/**
* 家庭成员
*/
class FamilyMember {
/** 员工 */
Employee employee
static belongsTo = [employee: Employee]
/** 关系:配偶、子女、其他(父亲、母亲...) */
String relationship
public static final String REL_MATE = "mate" // 配偶
public static final String REL_CHILDREN = "children" // 子女
/** 姓名 */
String name
/** 出生日期 */
Date birthdate
/** 民族 */
String nation
/** 户籍 */
String hukou
/** 政治面貌 */
String politicalStatus
/** 工作时间 */
Date workingDate
/** 学历 */
String education
/** 联系电话 */
String phone
/** 毕业院校及专业 */
String school
/** 工作单位 */
String company
/** 职务 */
String position
/** 性别 */
String gender
}
视图(或控制器)中,先根据 relationship 属性将所有家庭成员分类为配偶、子女和其他:
FamilyMember mate = employeeInstance.familyMembers.find{it.relationship == FamilyMember.REL_MATE} // 配偶
List<FamilyMember> children = employeeInstance.familyMembers.findAll{it.relationship == FamilyMember.REL_CHILDREN}.toList() // 子女
List<FamilyMember> others = (employeeInstance.familyMembers - children - mate).toList() // 其他
然后,视图开发时,只需要按照下面的步骤处理,即可使用动态表功能实现分类显示与自动保存:
- 将视图分为三段:配偶、子女、其他,每段显示和处理不同的数据。如配偶段处理对象 FamilyMamber mate,子女段处理列表 children、其他段处理列表 others
- 只有子女和其他两类使用了动态表标签 g:dynamicTable,其前缀均设为 fm(通过checkboxField属性),各个字段均以 fm 开头,如 fm.name、fm.gender 表示家庭成员的名称、性别;
- 而配偶段同样采用 fm 前缀来命名HTML元素,以便后台自动解析、合并
- 两个动态表中,为了全选的 checkbox 和 增/删行 的功能运行正常,动态表名称设为不一样,一个为 familyMembersChildren、familyMembersOthers
- 为了保证提交的顺序、数据对应关系正确,每段数据中都包含全部属性(即便没有使用)的HTML元素:
- 如配偶段,使用 g:hiddenField 将 fm.relationship 设置为 FamilyMember.REL_MATE
- 如子女段,将没有使用的 fm.nation、fm.hukou 等字段均设为空
完整的视图代码如下:
<tr class="prop">
<td class="name" colspan="4">配偶:</td>
</tr>
<tr class="prop">
<td class="name"><label for="fm.name"><g:message code="bropen.erp.hr.employee.FamilyMember.name" default="Name" />:</label></td>
<td class="value">
<g:formField bean="${employeeInstance}" name="familyMembers" value="${mate?.name}">
<g:hiddenField name="fm.id" value="${mate?.id ?: -1}" />
<g:hiddenField name="fm.relationship" value="${FamilyMember.REL_MATE}" />
<g:hiddenField name="fm.gender" value="" />
<g:textField name="fm.name" value="${mate?.name}" maxlength="${FamilyMember.constraints.name.maxSize}" />
</g:formField>
</td>
<td class="name"><label for="fm.birthdate"><g:message code="bropen.erp.hr.employee.FamilyMember.birthdate" default="Birthdate" />:</label></td>
<td class="value">
<g:formField bean="${employeeInstance}" name="familyMembers" value="${DateUtils.formatDate(mate?.birthdate)}">
<g:datePicker2 name="fm.birthdate" value="${birthdate?.name}" />
</g:formField>
</td>
</tr>
<tr class="prop">
<td class="name"><label for="fm.nation"><g:message code="bropen.erp.hr.employee.FamilyMember.nation" default="Nation" />:</label></td>
<td class="value">
<g:formField bean="${employeeInstance}" name="familyMembers" value="${mate?.nation}">
<g:textField name="fm.nation" value="${mate?.nation}" maxlength="${FamilyMember.constraints.nation.maxSize}" />
</g:formField>
</td>
<td class="name"><label for="fm.hukou"><g:message code="bropen.erp.hr.employee.FamilyMember.hukou" default="Hukou" />:</label></td>
<td class="value">
<g:formField bean="${employeeInstance}" name="familyMembers" value="${mate?.hukou}">
<g:textField name="fm.hukou" value="${mate?.hukou}" maxlength="${FamilyMember.constraints.hukou.maxSize}" />
</g:formField>
</td>
</tr>
<tr class="prop">
<td class="name"><label for="fm.politicalStatus"><g:message code="bropen.erp.hr.employee.FamilyMember.politicalStatus" default="Political Status" />:</label></td>
<td class="value">
<g:formField bean="${employeeInstance}" name="familyMembers" value="${mate?.politicalStatus}">
<g:textField name="fm.politicalStatus" value="${mate?.politicalStatus}" maxlength="${FamilyMember.constraints.politicalStatus.maxSize}" />
</g:formField>
</td>
<td class="name"><label for="fm.workingDate"><g:message code="bropen.erp.hr.employee.FamilyMember.workingDate" default="WorkingDate" />:</label></td>
<td class="value">
<g:formField bean="${employeeInstance}" name="familyMembers" value="${DateUtils.formatDate(mate?.workingDate)}">
<g:datePicker2 name="fm.workingDate" value="${mate?.workingDate}" />
</g:formField>
</td>
</tr>
<tr class="prop">
<td class="name"><label for="fm.education"><g:message code="bropen.erp.hr.employee.FamilyMember.education" default="Education" />:</label></td>
<td class="value">
<g:formField bean="${employeeInstance}" name="familyMembers" value="${mate?.education}">
<g:textField name="fm.education" value="${mate?.education}" maxlength="${FamilyMember.constraints.education.maxSize}" />
</g:formField>
</td>
<td class="name"><label for="fm.phone"><g:message code="bropen.erp.hr.employee.FamilyMember.phone" default="Phone" />:</label></td>
<td class="value">
<g:formField bean="${employeeInstance}" name="familyMembers" value="${mate?.phone}">
<g:textField name="fm.phone" value="${mate?.phone}" maxlength="${FamilyMember.constraints.phone.maxSize}" />
</g:formField>
</td>
</tr>
<tr class="prop">
<td class="name"><label for="fm.school"><g:message code="bropen.erp.hr.employee.FamilyMember.school" default="School" />:</label></td>
<td class="value" colspan="3">
<g:formField bean="${employeeInstance}" name="familyMembers" value="${mate?.school}">
<g:textField name="fm.school" value="${mate?.school}" maxlength="${FamilyMember.constraints.school.maxSize}" class="max" />
</g:formField>
</td>
</tr>
<tr class="prop">
<td class="name"><label for="fm.company"><g:message code="bropen.erp.hr.employee.FamilyMember.company" default="Company" />:</label></td>
<td class="value">
<g:formField bean="${employeeInstance}" name="familyMembers" value="${mate?.company}">
<g:textField name="fm.company" value="${mate?.company}" maxlength="${FamilyMember.constraints.company.maxSize}" class="max" />
</g:formField>
</td>
<td class="name"><label for="fm.position"><g:message code="bropen.erp.hr.employee.FamilyMember.position" default="Position" />:</label></td>
<td class="value">
<g:formField bean="${employeeInstance}" name="familyMembers" value="${mate?.position}">
<g:textField name="fm.position" value="${mate?.position}" maxlength="${FamilyMember.constraints.position.maxSize}" />
</g:formField>
</td>
</tr>
<tr class="prop">
<td class="name" colspan="4">子女:</td>
</tr>
<tr class="prop">
<td class="value" colspan="4">
<g:if test="${isFormFieldEditable(bean: employeeInstance, name: 'familyMembers') || children}">
<g:dynamicTable name="familyMembersChildren" class="innerTable" border="1" button="top" checkboxField="fm.id"
childProperty="familyMembers" requiredField="name"
editable="${isFormFieldEditable(bean: employeeInstance, name: 'familyMembers')}">
<thead>
<tr>
<th width="15" class="editable"><input type="checkbox" onclick="familyMembersChildren.selectAll(this.checked)"/></th>
<th width="100"><g:message code="bropen.erp.hr.employee.FamilyMember.name" default="Name" /></th>
<th width="50"><g:message code="bropen.erp.hr.employee.FamilyMember.gender" default="Gender" /></th>
<th width="100"><g:message code="bropen.erp.hr.employee.FamilyMember.birthdate" default="Birthdate" /></th>
<th width="300"><g:message code="bropen.erp.hr.employee.FamilyMember.company2" default="Company" /></th>
</tr>
</thead>
<tbody class="template">
<tr>
<td class="center">
<input name="fm.id" type="checkbox" value="-1" />
<input name="fm.relationship" type="hidden" value="${FamilyMember.REL_CHILDREN}" />
<input name="fm.nation" type="hidden" value="" />
<input name="fm.hukou" type="hidden" value="" />
<input name="fm.politicalStatus" type="hidden" value="" />
<input name="fm.workingDate" type="hidden" value="" />
<input name="fm.education" type="hidden" value="" />
<input name="fm.phone" type="hidden" value="" />
<input name="fm.school" type="hidden" value="" />
<input name="fm.position" type="hidden" value="" />
</td>
<td><g:textField name="fm.name" /></td>
<td><g:select name="fm.gender" from="${FamilyMember.constraints.gender.inList}" valueMessagePrefix="bropen.erp.hr.employee.FamilyMember.gender" noSelection="['':'']" /></td>
<td><input type="text" name="fm.birthdate" class="date" /></td>
<td><g:textField name="fm.company" style="width: 300px" /></td>
</tr>
</tbody>
<tbody>
<g:each in="${CollectionUtils.sort(children, 'asc', 'birthdate')}" var="fm">
<tr>
<td class="center editable">
<input name="fm.id" type="checkbox" value="${fm.id}"/>
<input name="fm.relationship" type="hidden" value="${FamilyMember.REL_CHILDREN}" />
<input name="fm.nation" type="hidden" value="" />
<input name="fm.hukou" type="hidden" value="" />
<input name="fm.politicalStatus" type="hidden" value="" />
<input name="fm.workingDate" type="hidden" value="" />
<input name="fm.education" type="hidden" value="" />
<input name="fm.phone" type="hidden" value="" />
<input name="fm.school" type="hidden" value="" />
<input name="fm.position" type="hidden" value="" />
</td>
<td><g:formField bean="${fm}" name="name">
<g:textField name="fm.name" value="${fm.name}" />
</g:formField></td>
<td><g:formField bean="${fm}" name="gender" valueI18n="true">
<g:select name="fm.gender" from="${FamilyMember.constraints.gender.inList}" valueMessagePrefix="bropen.erp.hr.employee.FamilyMember.gender" noSelection="['':'']" value="${fm.gender}" />
</g:formField></td>
<td><g:formField bean="${fm}" name="birthdate">
<input type="text" name="fm.birthdate" class="date" value="${DateUtils.formatDate(fm.birthdate)}" />
</g:formField></td>
<td><g:formField bean="${fm}" name="company">
<g:textField name="fm.company" style="width:300px" value="${fm.company}"/>
</g:formField></td>
</tr>
</g:each>
<tbody>
</g:dynamicTable>
</g:if><g:else>无</g:else>
</td>
</tr>
<tr class="prop">
<td class="name" colspan="4">其他成员:</td>
</tr>
<tr class="prop">
<td class="value" colspan="4">
<g:if test="${isFormFieldEditable(bean: employeeInstance, name: 'familyMembers') || others}">
<g:dynamicTable name="familyMembersOthers" class="innerTable" border="1" button="top" checkboxField="fm.id"
childProperty="familyMembers" requiredField="name"
editable="${isFormFieldEditable(bean: employeeInstance, name: 'familyMembers')}">
<thead>
<tr>
<th width="15" class="editable"><input type="checkbox" onclick="familyMembersOthers.selectAll(this.checked)"/></th>
<th width="50"><g:message code="bropen.erp.hr.employee.FamilyMember.relationship" default="Relationship" /></th>
<th width="100"><g:message code="bropen.erp.hr.employee.FamilyMember.name" default="Name" /></th>
<th width="100"><g:message code="bropen.erp.hr.employee.FamilyMember.birthdate" default="Birthdate" /></th>
<th width="50"><g:message code="bropen.erp.hr.employee.FamilyMember.politicalStatus" default="Political Status" /></th>
<th width="200"><g:message code="bropen.erp.hr.employee.FamilyMember.company" default="Company" /></th>
<th width="50"><g:message code="bropen.erp.hr.employee.FamilyMember.position" default="Position" /></th>
</tr>
</thead>
<tbody class="template">
<tr>
<td class="center">
<input name="fm.id" type="checkbox" value="-1" />
<input name="fm.nation" type="hidden" value="" />
<input name="fm.hukou" type="hidden" value="" />
<input name="fm.workingDate" type="hidden" value="" />
<input name="fm.education" type="hidden" value="" />
<input name="fm.phone" type="hidden" value="" />
<input name="fm.school" type="hidden" value="" />
<input name="fm.gender" type="hidden" value="" />
</td>
<td><g:textField name="fm.relationship" style="width: 50px" /></td>
<td><g:textField name="fm.name" /></td>
<td><input type="text" name="fm.birthdate" class="date" /></td>
<td><g:textField name="fm.politicalStatus" style="width: 50px" /></td>
<td><g:textField name="fm.company" style="width: 200px" /></td>
<td><g:textField name="fm.position" style="width: 50px" /></td>
</tr>
</tbody>
<tbody>
<g:each in="${CollectionUtils.sort(others, 'asc', 'name')}" var="fm">
<tr>
<td class="center editable">
<input name="fm.id" type="checkbox" value="${fm.id}"/>
<input name="fm.nation" type="hidden" value="" />
<input name="fm.hukou" type="hidden" value="" />
<input name="fm.workingDate" type="hidden" value="" />
<input name="fm.education" type="hidden" value="" />
<input name="fm.phone" type="hidden" value="" />
<input name="fm.school" type="hidden" value="" />
<input name="fm.gender" type="hidden" value="" />
</td>
<td><g:formField bean="${fm}" name="relationship">
<g:textField name="fm.relationship" style="width: 50px" value="${fm.relationship}" />
</g:formField></td>
<td><g:formField bean="${fm}" name="name">
<g:textField name="fm.name" value="${fm.name}" />
</g:formField></td>
<td><g:formField bean="${fm}" name="birthdate">
<input type="text" name="fm.birthdate" class="date" value="${DateUtils.formatDate(fm.birthdate)}" />
</g:formField></td>
<td><g:formField bean="${fm}" name="politicalStatus">
<g:textField name="fm.politicalStatus" style="width: 50px" value="${fm.politicalStatus}" />
</g:formField></td>
<td><g:formField bean="${fm}" name="company">
<g:textField name="fm.company" style="width: 200px" value="${fm.company}"/>
</g:formField></td>
<td><g:formField bean="${fm}" name="position">
<g:textField name="fm.position" style="width: 50px" value="${fm.position}" />
</g:formField></td>
</tr>
</g:each>
<tbody>
</g:dynamicTable>
</g:if><g:else>无</g:else>
</td>
</tr>
最终效果如下图所示:
