
以上图片中间部分是个自动生成的form,不用专门给它写代码或用“表单设计器”工具手工生成代码。这种方法绝非rocket science,只是scaffolding雕虫小技而已。
Grails的scaffolding生成的是HTML代码,而不是JSON形式的form定义,无法直接跟Ext无缝整合。
为Ext写个form scaffolding并非难事。跟Grails的scaffolding原理一致,表单中的fields是从domain class中提取出来的,排序则按照它们在constraints中出现的先后顺序。
先看一个domain class的示例:
class SiteContactLog {
@Hidden // Gotta hide it, otherwise it will be rendered as a combobox by default. And loading all sites is time-consuming
Site site
@Hidden
User user
Date contactDate
@Xtype(xtype='timefield')
String contactTime
@SelectionItemFetcher(fetcher="{d->app.getArtefact('Domain','com.xxx.sctms.Person').clazz.findAll('site.id':d._belongsTo)}")
@LiveSearch(searchBy='name')
List<Person> personsContacted
@SelectionItemFilter(filter="{d,q->q.descend('group').constrain('siteContactCorrespondence');q.descend('name').orderAscending()}")
GenericSelectionItem correspondence
@SelectionItemFilter(filter="{d,q->q.descend('group').constrain('siteContactDirection');q.descend('name').orderAscending()}")
GenericSelectionItem direction
@SelectionItemFilter(filter="{d,q->q.descend('group').constrain('siteContactMethod');q.descend('name').orderAscending()}")
GenericSelectionItem contactMethod
@SelectionItemFilter(filter="{d,q->q.descend('group').constrain('siteContactTopic');q.descend('seq').orderAscending()}")
GenericSelectionItem topic
String summary
static constraints = {
contactDate nullable:false
contactTime()
personsContacted nullable:false, minSize:1
correspondence nullable:false
direction()
contactMethod()
topic()
summary nullable:false, blank:false, maxSize:Integer.MAX_VALUE
}
}
基于domain生成form的机理用以下两个代码片段说明:
static retrieveDomainFields(domain) {
def artefact = getDomainArtefact(domain)
def fields = []
def constrainedProperties = artefact.constrainedProperties
artefact.properties.each{
if(!("${it.name}" ==~ /id|parentId|version|dateCreated|createdBy|deleted|dateDeleted|deletedBy/)) {
def ext = [:]
def declaredField = artefact.clazz.getDeclaredField(it.name)
def label = declaredField.getAnnotation(com.xxx.annotations.Label.class)
def formattedName = label?.label() ?: getFormattedNameByConvention(it.name)
// extra configurations from annotations:
def embeddedObject = declaredField.getAnnotation(com.xxx.annotations.EmbeddedObject.class)
if(embeddedObject) {
ext['embeddedObject'] = [height: embeddedObject.height(), width: embeddedObject.width()]
}
def singleCheckBox = declaredField.getAnnotation(com.xxx.annotations.SingleCheckBox.class)
if(singleCheckBox) {
ext['singleCheckBox'] = true
}
def radio = declaredField.getAnnotation(com.xxx.annotations.Radio.class)
if(radio) {
ext['radio'] = true
}
def liveSearch = declaredField.getAnnotation(com.xxx.annotations.LiveSearch.class)
if(liveSearch) {
ext['liveSearch'] = true
}
def hidden = declaredField.getAnnotation(com.xxx.annotations.Hidden.class)
if(hidden) {
ext['hidden'] = true
}
def xtype = declaredField.getAnnotation(com.xxx.annotations.Xtype.class)
if(xtype) {
ext['xtype'] = xtype.xtype()
}// TODO: inject more extensions from other annotations...
ext['declaredField'] = declaredField
if(constrainedProperties."${it.name}") {
if(constrainedProperties."${it.name}".maxSize) {
ext['maxSize'] = constrainedProperties."${it.name}".maxSize
}
}
// fields << [it.name, formattedName, formattedName, it.type, ext] // name, formattedName, description, type, ext
fields << [it.name, formattedName, formattedName, ext] // name, formattedName, description, ext (declaredField has been added to ext, do we still need it.type?)
}
}
// sort by the sequence they appear in the constraints
fields.sort{f1,f2->
def cp1 = constrainedProperties[f1[0]]
def cp2 = constrainedProperties[f2[0]]
cp1?(cp2?cp1.order<=>cp2.order:1):(cp2?-1:0)
}
fields
}
private buildFormScaffold(domain, fields) {
def artefact = DomainUtils.getDomainArtefact(domain)
def dn = artefact.clazz.name
def maxCaptionWidthAndControlsTotalHeight = processFields(fields)
def captionWidth = maxCaptionWidthAndControlsTotalHeight[0], captionHeight = 22, controlsTotalHeight = maxCaptionWidthAndControlsTotalHeight[1]
def width = COMPONENT_WIDTH, top = FORM_MARGIN, left = captionWidth + FORM_MARGIN, tabIndex = 100
def processingRightHalf = false, leftTop = 0, rightTop = 0
def leftControlsTotalHeight = 0
def scaffold = [
"id":"${UUID.randomUUID()}",
"xtype":"form",
"layout":"absolute",
"name":dn,
"height":0, //TBD
"width":(captionWidth + width + FORM_MARGIN*2)*2,
"title":"FORM SCAFFOLD",
"items":[]]
for(int i = 0; i < fields.size(); i++) {
if(leftControlsTotalHeight*2 >= controlsTotalHeight && !processingRightHalf) {
// reset top and left
processingRightHalf = true
top = FORM_MARGIN
left += (width + FORM_MARGIN * 2 + captionWidth)
}
def field = fields[i]
// e.g., Site has a 'study' field, and site belongs to study
// the study field should not show up
if(isBelongsToField(dn, field[0]) || field[3]['hidden'])continue
def htmlType = field[3].xtype ?: field[3].htmlType
def nullableConstraint = artefact.constraints."${field[0]}".getAppliedConstraint('nullable')
scaffold.items << [
xtype:htmlType,id:"${UUID.randomUUID()}",name:field[0],
allowBlank:nullableConstraint ? nullableConstraint.isNullable() : true,
forceSelection:false,
readOnly:false,
captionText:field[1],captionPosition:"Left",captionHeight:captionHeight,captionWidth:captionWidth,
x:(left-(htmlType=='placeHolder'?captionWidth:0)),y:top,
height:field[3].height,width:(width+(htmlType=='placeHolder'?captionWidth:0)),tabIndex:tabIndex]
if(htmlType == 'datefield') {
scaffold.items[-1].format = DATE_FORMAT
} else if(htmlType == 'radiogroup') {
if(field[3].singleCheckBox) {
scaffold.items[-1].xtype = 'checkbox'
scaffold.items[-1].singleCheckBox = true
scaffold.items[-1].xtype = "checkbox"
scaffold.items[-1].boxLabel = field[1]
scaffold.items[-1].y += 4
}
}
tabIndex += 100
top += (field[3].height + 4)
if(processingRightHalf) {
rightTop = top
} else {
leftControlsTotalHeight += field[3].height
leftTop = top
}
}
scaffold.height = (leftTop > rightTop ? leftTop : rightTop) + (FORM_MARGIN-4)
scaffold as JSON
}
生成的form形如
{
"id" : "a4fe0cda-ec31-4ec0-a809-bdbe73891fdf", "xtype" : "form", "layout" : "absolute", "name" : "com.xxx.sctms.SiteContactLog", "height" : 236, "width" : 832, "title" : "FORM SCAFFOLD", "items" : [ {
"xtype" : "datefield", "id" : "f9a7a0ab-6799-473c-b811-7a6957ea95cc", "name" : "contactDate", "allowBlank" : false, "forceSelection" : false, "readOnly" : false, "captionText" : "Contact Date", "captionPosition" : "Left", "captionHeight" : 22, "captionWidth" : 136, "x" : 151, "y" : 15, "height" : 22, "width" : 250, "tabIndex" : 100, "format" : "M/d/Y"}
, {
"xtype" : "timefield", "id" : "838ec058-4ccb-4134-81df-c77d001365da", "name" : "contactTime", "allowBlank" : true, "forceSelection" : false, "readOnly" : false, "captionText" : "Contact Time", "captionPosition" : "Left", "captionHeight" : 22, "captionWidth" : 136, "x" : 151, "y" : 41, "height" : 22, "width" : 250, "tabIndex" : 200}
, {
"xtype" : "checkboxgroup", "id" : "7ad425bc-49e4-4c93-9051-a42c7a42d197", "name" : "personsContacted", "allowBlank" : false, "forceSelection" : false, "readOnly" : false, "captionText" : "Persons Contacted", "captionPosition" : "Left", "captionHeight" : 22, "captionWidth" : 136, "x" : 151, "y" : 67, "height" : 102, "width" : 250, "tabIndex" : 300}
, {
"xtype" : "combo", "id" : "34e2a5fd-c37d-41b5-ad26-4c98ec5a91f6", "name" : "correspondence", "allowBlank" : false, "forceSelection" : false, "readOnly" : false, "captionText" : "Correspondence", "captionPosition" : "Left", "captionHeight" : 22, "captionWidth" : 136, "x" : 151, "y" : 173, "height" : 22, "width" : 250, "tabIndex" : 400}
, {
"xtype" : "combo", "id" : "c146e14d-2ac7-4cbd-8648-55c3f62bb306", "name" : "direction", "allowBlank" : true, "forceSelection" : false, "readOnly" : false, "captionText" : "Direction", "captionPosition" : "Left", "captionHeight" : 22, "captionWidth" : 136, "x" : 151, "y" : 199, "height" : 22, "width" : 250, "tabIndex" : 500}
, {
"xtype" : "combo", "id" : "93070ee3-1def-4c93-b7c1-2a722ed20409", "name" : "contactMethod", "allowBlank" : true, "forceSelection" : false, "readOnly" : false, "captionText" : "Contact Method", "captionPosition" : "Left", "captionHeight" : 22, "captionWidth" : 136, "x" : 567, "y" : 15, "height" : 22, "width" : 250, "tabIndex" : 600}
, {
"xtype" : "combo", "id" : "a3a427dd-c499-4378-9e63-f9a3d59e2958", "name" : "topic", "allowBlank" : true, "forceSelection" : false, "readOnly" : false, "captionText" : "Topic", "captionPosition" : "Left", "captionHeight" : 22, "captionWidth" : 136, "x" : 567, "y" : 41, "height" : 22, "width" : 250, "tabIndex" : 700}
, {
"xtype" : "textarea", "id" : "ef85d8cb-50e6-4508-9d9c-83056208427e", "name" : "summary", "allowBlank" : false, "forceSelection" : false, "readOnly" : false, "captionText" : "Summary", "captionPosition" : "Left", "captionHeight" : 22, "captionWidth" : 136, "x" : 567, "y" : 67, "height" : 102, "width" : 250, "tabIndex" : 800}
]}
这样生成的form排版比较单一,分两栏,高度大致相等。若需修改,可使用表单设计器...
本文介绍了一种基于Grails的scaffolding理念为ExtJS自动生成表单的方法。通过从领域模型中提取字段信息,并根据约束顺序排列,实现表单的自动化构建。此方法不仅简化了开发流程,还确保了表单与模型的一致性。
193

被折叠的 条评论
为什么被折叠?



