Lift - Autocomplete with Ajax Submission

callback, jquery-ui, lift, scala

Solution

Here's what you need to do.

1) Make sure you are using Lift 2.5-SNAPSHOT (this is doable in earlier versions, but it's more difficult)

2) In the snippet you use to render the page, use SHtml.ajaxCall (in particular, you probably want this version: https://github.com/lift/framework/blob/master/web/webkit/src/main/scala/net/liftweb/http/SHtml.scala#L170) which will allow you to register a server side function that accepts your search term and return a JSON response containing the completions. You will also register some action to occur on the JSON response with the JsContext.

3) The ajaxCall above will return a JsExp object which will result in the ajax request when it's invoked. Embed it within a javascript function on the page using your snippet.

4) Wire them up with some client side JS.

Update - Some code to help you out. It can definitely be done more succinctly with Lift 2.5, but due to some inconsistencies in 2.4 I ended up rolling my own ajaxCall like function. S.fmapFunc registers the function on the server side and the function body makes a Lift ajax call from the client, then invokes the res function (which comes from jQuery autocomplete) on the JSON response.

My jQuery plugin to "activate" the text input

(function($) {
    $.fn.initAssignment = function() {
     return this.autocomplete({
         autoFocus: true,
         source: function(req, res) {
              search(req.term, res);
         },
         select: function(event, ui) {
             assign(ui.item.value, function(data){
                eval(data);
             });
             event.preventDefault();
             $(this).val("");
         },
         focus: function(event, ui) {
             event.preventDefault();
         }
     });
    }
})(jQuery);

My Scala code that results in the javascript search function:

def autoCompleteJs = JsRaw("""
        function search(term, res) {
        """ +
             (S.fmapFunc(S.contextFuncBuilder(SFuncHolder({ terms: String =>
                val _candidates = 
                  if(terms != null && terms.trim() != "")
                    assigneeCandidates(terms)
                  else
                    Nil
                JsonResponse(JArray(_candidates map { c => c.toJson }))
             })))
             ({ name => 
               "liftAjax.lift_ajaxHandler('" + name 
             })) + 
             "=' + encodeURIComponent(term), " +
             "function(data){ res(data); }" +
             ", null, 'json');" +
        """
        }
        """)

Update 2 - To add the function above to your page, use a CssSelector transform similar to the one below. The >* means append to anything that already exists within the matched script element. I've got other functions I've defined on that page, and this adds the search function to them.

"script >*" #> autoCompleteJs

You can view source to verify that it exists on the page and can be called just like any other JS function.

Problem

I would like to use an autocomplete with ajax. So my goal is to have: When the user types something in the text field, some suggestions provided by the server appear (I have to find suggestions in a database) When the user presses "enter", clicks somewhere else than in the autocomplete box, or when he/she selects a suggestion, the string in the textfield is sent to the server. I first tried to use the autocomplete widget provided by lift but I faced three problems: - it is meant to be an extended select, that is to say you can originally only submit suggested values. - it is not meant to be used with ajax. - it gets buggy when combined with `WiringUI`. So, my question is: How can I combine jquery autocomplete and interact with the server in lift. I think I should use some callbacks but I don't master them. Thanks in advance. UPDATE Here is a first implementation I tried but the callback doesn't work: ``` private def update_source(current: String, limit: Int) = { val results = if (current.length == 0) Nil else /* generate list of results */ new JsCmd{def toJsCmd = if(results.nonEmpty) results.mkString("[\"", "\", \"", "\"]") else "[]" } } def render = { val id = "my-autocomplete" val cb = SHtml.ajaxCall(JsRaw("request"), update_source(_, 4)) val script = Script(new JsCmd{ def toJsCmd = "$(function() {"+ "$(\"#"+id+"\").autocomplete({ "+ "autocomplete: on, "+ "source: function(request, response) {"+ "response("+cb._2.toJsCmd + ");" + "}"+ "})});" }) <head><script charset="utf-8"> {script} </script></head> ++ <span id={id}> {SHtml.ajaxText(init, s=>{ /*set cell to value s*/; Noop}) } </span> } ``` So my idea was: - to get the selected result via an `SHtml.ajaxText` field which would be wraped into an autocomplete field - to update the autocomplete suggestions using a javascript function

Original source