I have altered the src/templates/scaffolding/renderEditor.template file in a grails project, in order to insert, the necessary html field boxes (and javascript code) to configure and use jquery autocomplete, in all the relationships "many-to-one". (The code is shown below)
The autogenerated autocomplete (_form.gsp) works correctly ... but I need to show the correct values (code and description) into the autocomplete textbox when a user edit a record using the scaffolding.
To do that, I need to identify two fields inside the domain: one for code and the other for the description.
To face this problem, I have tried to create two dummies constraints, using the plugin "constraints", the first one used like code, the second one used like a description. I don't like this solution, because the contrainsts could be used several times into the domain.
The code altered into the src/templates/scaffolding/renderEditor.template file is the following: (Note the two input boxes and Javascript code used for autocomplete):
private renderManyToOne(domainClass, property) {
if (property.association) {
/* ORIGINAL CODE inside comments
def sb = new StringBuilder()
sb << '<g:select'
// id is "x" and name is "x.id" as the label will have for="x" and "." in an id will confuse CSS
sb << ' id="' << property.name << '"'
sb << ' name="' << property.name << '.id"'
sb << ' from="${' << property.type.name << '.list()}"'
sb << ' optionKey="id"'
if (isRequired()) sb << ' required=""'
sb << ' value="${' << "${domainInstance}?.${property.name}" << '?.id}"'
sb << ' class="many-to-one"'
sb << renderNoSelection(property)
sb << '/>'
sb as String
*/
def sb = new StringBuilder()
// hidden field for domain.id
sb << '<input type=\"hidden\" '
sb << ' id="' << property.name << '.id"'
sb << ' name="' << property.name << '.id"'
sb << ' value="${' << "${domainInstance}" << '?.id}" '
sb << '/>\n'
// Text field to show the description generated by autocomplete
sb << '\t<input type=\"text\" '
sb << ' id="' << property.name << '"'
sb << ' name="' << property.name << '"'
if (isRequired()) sb << ' required="" '
sb << 'style=\"width: 600px;\" '
sb << ' value="${' << "${domainInstance}?.${property.name}" << '?.id}"'
// sb << '${' << "${property.name}" << '"'
sb << '/>'
def js = new StringBuilder()
js << '''
<script type="text/javascript">
/*
* Remember include jquery and jquery-ui libraries into head section of edit.gsp file
* < g:javascript library="jquery"/>
* < g:javascript library="jquery-ui"/>
*/
\$(document).ready(function() {
'''
js << '\t\$("#' << property.name << '").focus(function(){this.select(); });\n'
js << '\t\t$("#' << property.name << '").autocomplete({\n'
js << '''
source: function(request, response){
\$.ajax({
// Define Remote datasource into the controller
'''
js << ' \t\t url: "'
js << '/' << grails.util.Metadata.current.'app.name' << '/' << property.name << '/' << 'autoCompleteList",'
js << '''
data: request,
success: function(data){
// Get the response (JSON format)
response(data);
},
error: function(){
// Handle server errors
response("Error after search records. Try Again.")
}
});
},
// General options: Triggered only after minimum 2 characters have been entered and others
minLength: 2,
delay: 1,
autoFocus: true,
// Event handler when user selects a Loinc from the list.
select: function(event, ui) {
// update the hidden field.
'''
js << '\t\t\t\t \$("#' << property.name << '\\.id").val(ui.item.id);'
js << '''
}
});
});
</script>
'''
sb << js
sb as String
}
The domain using the dummy contraints (autoid and autodesc):
class LOINC {
static searchable = {
only = ["code", "shortName", "longName", "property", "system", "scale", "method", "time"]
}
String code // LOINC_NUM * 0
String shortName // SHORTNAME * 29
String longName // LONG_COMMON_NAME * 35
String name // BASE_NAME * 21
String component // COMPONENT * 1
String property // PROPERTY * 2
String time // TIME_ASPCT * 3
String system // SYSTEM * 4
String scale // SCALE_TYP * 5
String method // METHOD_TYP * 6
static constraints = {
code(nullable: false, unique: true, blank: false, maxSize: 100, autoid: true)
shortName(nullable: false)
longName(nullable: false, autodesc: true)
name(nullable: false, maxSize: 100)
component(nullable: false)
property(nullable: false)
time(nullable: false)
system(nullable: false)
scale(nullable: false)
method(nullable: false)
}
String toString(){
"${code} ${longName}"
}
}
The code inside the controler:
def autoCompleteList = {
def loincAutoCompleteService = new LOINCAutoCompleteService()
render loincAutoCompleteService.loincList(params) as JSON
}
The service:
class LOINCAutoCompleteService {
def loincList(params) {
// Creates a new query Object
def query = {
or {
like("code", "${params.term}%") // term is the parameter send by jQuery autocomplete
like("longName", "${params.term}%")
like("shortName", "${params.term}%")
}
projections { // good to select only the required columns.
property("id")
property("code")
property("longName")
}
}
def loincSelectList = [] // aArray List to add each Loinc details
def clist = LOINC.createCriteria().list(query)
clist.each {
// Add to map. jQuery autocomplete expects the JSON object to be with id/label/value
def loincMap = [:]
loincMap.put("id", it[0])
// Label is text showed int he drop-down list
loincMap.put("label", it[1] + " : " + it[2])
// Values is the code to be returned when the user select an item from drop-down list
loincMap.put("value", it[1] + " : " + it[2])
// Add the row to the array list
loincSelectList.add(loincMap)
}
return loincSelectList
}
}
I want something like that inside the domain class:
<code>
static autocompleteAble = {
fields = ["code", "longName"]
}
</code>
Then access this array from the src/templates/scaffolding/renderEditor.template in order to get the field names (code and longName) and generate the correct html code in _forms.gsp and fix the problem.
Other solution? Any Ideas?
Many Thanks in Advance. ... and excuse my bad English.
Reading and testing and testing again ... I have found the answer, follow the steps:
Add the following code to the Domain class:
class DomainClass { String codeField; String descriptionField; static autoCompleteConfig = ["codeField", "descriptionField"] }
Change the src/templates/scaffolding/renderEditor.template (only the renderManyToOne method):
private renderManyToOne(domainClass, property) {
def AUTOCOMPLETE_PROPERTY = "autoCompleteConfig"
def className = property.type.name
def autoCompleteProperty = org.codehaus.groovy.grails.commons.GrailsClassUtils.getStaticPropertyValue(property.referencedDomainClass.clazz, AUTOCOMPLETE_PROPERTY)
def sb = new StringBuilder()
// sb << "\n<!-- getFullName(): " << domainClass.getFullName() << " property.type.name: " << property.type.name << " property.referencedDomainClass.propertyName: " << property.referencedDomainClass.propertyName << " property.referencedDomainClass: " << property.referencedDomainClass << " -->\n"
if (autoCompleteProperty != null) {
if (autoCompleteProperty[0] ) {
if (property.association) {
// hidden field for domain.id
sb << '<input type=\"hidden\" '
sb << ' id= "' << property.name << '.id"'
sb << ' name="' << property.name << '.id"'
sb << ' value="${' << "${domainInstance}" << '?.id}" '
sb << '/>\n'
// Text field to show the description generated by autocomplete
sb << '\t<input type=\"text\" '
sb << ' id= "' << property.name << '_' << (autoCompleteProperty[1]? autoCompleteProperty[1]:'Description') << '\" '
sb << ' name="' << property.name << '_' << (autoCompleteProperty[1]? autoCompleteProperty[1]:'Description') << '\" '
if (isRequired()) sb << ' required="" '
sb << 'style=\"width: 600px;\" '
sb << ' value="${'
sb << "${domainInstance}?.${property.name}" << '?.' << autoCompleteProperty[0] << '}' << (autoCompleteProperty[1]? '' : '"' )
if (autoCompleteProperty[1]) {
sb << ': ${' << "${domainInstance}?.${property.name}" << '?.' << autoCompleteProperty[1] << (autoCompleteProperty[2]? '}' : '}"' )
}
if (autoCompleteProperty[2]) {
sb << ': ${' << "${domainInstance}?.${property.name}" << '?.' << autoCompleteProperty[2] << '}"'
}
sb << ' />'
def js = new StringBuilder()
js << '''
<script type="text/javascript">
/*
* Remember include jquery and jquery-ui libraries into head section of edit.gsp file
* < g:javascript library="jquery"/>
* < g:javascript library="jquery-ui"/>
*
*/
\$(document).ready(function() {
'''
js << '\t\$("#' << property.name << '").focus(function(){this.select(); });\n'
js << '\t\t\t\t\t\t' // Tabs to sort the output
js << '\$("#' << property.name << '_' << (autoCompleteProperty[1]? autoCompleteProperty[1]:'Description') << '").autocomplete({\n'
js << '''
source: function(request, response){
\$.ajax({
// Define Remote datasource into the controller
'''
js << ' \t\t url: "'
js << '/' << grails.util.Metadata.current.'app.name' << '/' << property.name << '/' << 'autoCompleteList",'
js << '''
data: request,
success: function(data){
// Get the response (JSON format)
response(data);
},
error: function(){
// Handle server errors
response("Error after search records. Try Again.")
}
});
},
// General options: Triggered only after minimum 2 characters have been entered and others
minLength: 2,
delay: 1,
autoFocus: true,
// Event handler when user choose un item from the list.
select: function(event, ui) {
// update the hidden field.
'''
js << '\t\t\t\t '
js << '\$("#' << property.name << '\\\\.id").val(ui.item.id);'
js << '''
}
});
});
</script>
'''
sb << js
sb as String
}
}
} else {
sb << '<g:select'
// id is "x" and name is "x.id" as the label will have for="x" and "." in an id will confuse CSS
sb << ' id="' << property.name << '"'
sb << ' name="' << property.name << '.id"'
sb << ' from="${' << property.type.name << '.list()}"'
sb << ' optionKey="id"'
if (isRequired()) sb << ' required=""'
sb << ' value="${' << "${domainInstance}?.${property.name}" << '?.id}"'
sb << ' class="many-to-one"'
sb << renderNoSelection(property)
sb << '/>'
sb as String
}
}
Add the jquery libraries to src/templates/scaffolding/edit.gsp. Remember to install the jquery plugin:
Write your own autoCompleteRoutine inside the Domain controller, something like:
def autoCompleteList = { def domainAutoCompleteService = new DomainAutoCompleteService() render domainAutoCompleteService.domainList(params) as JSON }
Write your own domainAutoCompleteService, something like:
package packageName
// Change the words "Domain" and "domain" with your own Domain class name
class DomainAutoCompleteService {
def domainList(params) {
// Creates a new query Object
def query = {
or {
// term is the parameter send by jQuery autocomplete
like("codeField", "${params.term}%")
like("descriptionField", "${params.term}%")
like("otherField", "${params.term}%")
}
projections { // good to select only the required columns.
property("id")
property("codeField")
property("descriptionField")
}
}
def domainSelectList = []
// Replace the word "Domain" by your own domain Name
def clist = Domain.createCriteria().list(query)
clist.each {
// Add to map. jQuery autocomplete expects the JSON object to be with id/label/value
def map = [:]
map.put("id", it[0])
// Label is text showed int he drop-down list
map.put("label", it[1] + " : " + it[2])
// Values is the code to be returned when the user select an item from drop-down list
map.put("value", it[1] + " : " + it[2])
// Add the row to the array list
domainSelectList.add(map)
}
return domainSelectList
}
}
Generate the views .... and voila! All is working.
Any Comments? I think can be more elegant but is the first step ...