Friday, December 16, 2011

Enabling jQuery autocomplete in Grails using Tag and Service

I often use autocomplete for multiple fields on my views. These fields might be from one domain or different domains. Instead of creating multiple actions to handle each request, I created a Tag and a Service to make things easier.

The idea is the jQuery will handle the front end by sending the request to controller and displaying the returned json results in a list. So what I just needed to do was to route that request to a service to get the json result and sent it back to the view. The autoComplete tag make it reusable and the service take care of the rest. This way I will just need to set the domains and property in the gsp. The rest will be handled consistently.

Example:
I want to add autocomplate to my author and isbn field on my view. Each coming from different domain. I just need to set my tags parameters and send it to autoComplete action in one controller.

Add the following tag in your gsp with the information about the domain and field:






Done!


Tag:


Service:

11 comments:

  1. Thanks Alidad, an amazing and very clean way of doing auto complete. I have got it working and I have one question. How easy would it be to make the second box of selection choice be based on value selected in first. Scenario country / city = once country selected to base city search on country id

    ReplyDelete
  2. I have managed to make it work by physically defining country id:
    --------------------------

    <label>Country:</label>
    <g:autoComplete id="id"
    action='autocompleteAction'
    controller='any'
    domain='rentspace.Country'
    searchField='name'
    value=''
    />

    <label>City:</label>
    <g:autoComplete id="countryId"
    countryid="2"
    action='autocompleteCityAction'
    controller='any'
    domain='rentspace.City'
    searchField='name'
    value=''
    />


    -----------------------------

    class AnyController {
    def autoCompleteService
    //AutoCompleteService autoCompleteService
    def autocompleteAction= {
    render autoCompleteService.autocompleteAction(params)
    }

    def autocompleteCityAction= {
    render autoCompleteService.autocompleteCityAction(params)
    }
    }

    -----------------

    import grails.converters.JSON

    class AutoCompleteService {
    def grailsApplication
    ..

    def autocompleteCityAction (params) {
    def domainClass = grailsApplication.getDomainClass(params.domain).clazz
    def results = domainClass.createCriteria().list {
    eq 'country.id', (Long.valueOf(params.countryid))
    ilike params.searchField, params.term + '%'
    maxResults(Integer.parseInt(params.max,10))
    order(params.searchField, params.order)
    }
    if (results.size()< 5){
    results = domainClass.createCriteria().list {
    eq 'country.id', (Long.valueOf(params.countryid))
    ilike params.searchField, "%${params.term}%"
    maxResults(Integer.parseInt(params.max,10))
    order(params.searchField, params.order)
    }
    }
    results = results.collect { [label:it."${params.collectField}"] }.unique()
    return results as JSON
    }

    }

    ---------------------------------

    class AutoCompleteTagLib {
    ...
    if (attrs.searchField == null)
    throwTagError("Tag [autoComplete] is missing required attribute [searchField]")

    def clazz = ""
    def name = ""
    def cid=""
    ....
    if (attrs.style) styles = " styles='${attrs.style}'"

    if (attrs.countryid)
    cid="&countryid="+attrs.countryid
    else
    cid=""

    ..
    out << "&searchField="+attrs.searchField
    out << "&max="+attrs.max
    out << "&order="+attrs.order
    out << ""+cid
    out << "&collectField="+attrs.collectField
    ...

    -----------------

    The only thing left now is how to get that defined countryid value set on 2nd drop box to be automatically picked from the first box..

    ReplyDelete
  3. What you can do is when you change the value of your first box 'country id' put that id in the parameter of the second autocomplete (city) using a javascript code. look at this example, there is a javascript function that will be called upon the change in the value of textfield in your case this is your first autocomplete, so when you make any changes to country id this method will set the attribute of 'myval' to the value of the country id. from that point in the tag library you can get that value from the attrs.myval and do whatever you want to do with it.

    Hope this helps.



    $('#message').change(function() {
    val = $('#message').val();
    $("#demo").attr('myval',val)
    });


    < g:textField id='message' name='message' value=""/ >
    < g:autoComplete id="demo" ... myval=""/ >

    ReplyDelete
  4. Thanks Alidad and I did get the above to work, but this is not what I was trying to achieve, basically the above so long as I provide a number to text field a field in the autocomplete can be set as that value, great to know how now but here is where I am stuck and perhaps you can shed more light on this:

    In the autoCompleteService I have written a new function:


    def autocompleteTest (params) {
    def domainClass = grailsApplication.getDomainClass(params.domain).clazz
    def query = {
    or{
    ilike params.searchField, params.term + '%'

    }
    projections { // good to select only the required columns.
    property(params.collectField)
    property(params.searchField)
    }
    maxResults(Integer.parseInt(params.max,10))
    order(params.searchField, params.order)
    }
    def query1 = {
    or{
    ilike params.searchField, "%${params.term}%"
    }

    projections { // good to select only the required columns.##
    property(params.collectField)
    property(params.searchField)

    }
    maxResults(Integer.parseInt(params.max,10))
    order(params.searchField, params.order)
    }

    def results =domainClass.createCriteria().list(query)
    if (results.size()< 5){
    results = domainClass.createCriteria().list(query1)
    }

    def countrySelectList = []
    results.each {
    def countryMap = [:]
    //countryMap.put(params.collectField, it[0])

    // countryMap.put(params.searchField, it[1])
    countryMap.put('temp', it[0])
    countryMap.put('label', it[1])
    countrySelectList.add(countryMap)
    }
    //return countrySelectList
    //results = results.collect { [label:it."${params.collectField}"] }.unique()
    return countrySelectList as JSON
    }


    The thing I have not understood is why return field mu be labelled 'label' and if I understood how it was reading this field in I could probably answer my own problem, because as you can see when the user selects a country the country id is also returned, initiall i tried calling them the field names so its dynamic according to what is set to collect and searchFields and although JSON returns both values only if set to label can I ee the auto complete function working.

    The question is how do i return the value set to temp currently to the second auto complete being country

    ReplyDelete
  5. "Label" is just how autocomplete expects the json data to be. before returning your list format it as label and values, like
    //results = results.collect { [label:it."${params.collectField}"] }.unique()

    ReplyDelete
  6. HI Alidad

    https://github.com/vahidhedayati/grailscountrycity

    I have managed to return both the countryid and countryname to the web page but as mentioned within the readme and aparent whilst running the code the countryid attribute does not appear to get updated,
    I could not figure out how to return either the javascript variable countryValue to the autocomplete tag lib for city or a way of getting the countryid attribute to update.

    The hiddenField does update with no issues

    All of the code is within the git project above and should be ready to use once downloaded

    Regards
    Vahid

    ReplyDelete
  7. Hey Vahid, take a look at here, if you had any question let me know.
    https://github.com/alidadasb/CountryCityAutoComplete

    ReplyDelete
  8. running app is also here: countrycity.cloudfoundry.com

    ReplyDelete
  9. Awesome Thanks Alidad.

    Warm regards
    Vahid

    ReplyDelete
  10. Hi Alidad, I have made this into a more generic plugin :

    http://grails.org/plugin/ajaxdependancyselection

    Thanks for all your help, I have mentioned you

    Regards
    Vahid


    ReplyDelete
  11. Awesome TagLib, thanks for the share.

    ReplyDelete