以上图片中间部分是个自动生成的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排版比较单一,分两栏,高度大致相等。若需修改,可使用表单设计器...