Lift - Autocomplete with Ajax Submission
Asked Answered
A

2

8

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
Ambassadress answered 6/4, 2012 at 11:58 Comment(0)
E
8

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.

Eisenhower answered 6/4, 2012 at 16:0 Comment(9)
Hi, unfortunately I cannot use Lift 2.5. I am on lift 2.4 M4. However, from what I have already used, it seems that there are already callbacks implemented in 2.4. Thanks for your answer.Ambassadress
I updated my question with the moment where I am now blocked, any suggestion is welcome.Ambassadress
I'm not sure what you mean when you say the callback doesn't work. Is update_source executed? If so, your problem is likely with your return. You are making an asynchronous call, so just returning the JSON isn't enough, the browser won't know what to do with it. You'll need to return a JsCmd that performs an action, and the result of that action should be population of the JQuery UI autocomplete.Eisenhower
I understand what you mean, with the code I posted, the cell update works. Do you think it is sufficient to modify the function update_source, if so, can you give me some code? Javascript is not my strong point.Ambassadress
Your edit is impressing, what it is possible to do to link javascript and lift is incredible. However I am a bit lost and I don't know where to use the function autoCompleteJs in my code. Do I have to keep the function script and use autoCompleteJs as ajaxCall(autoCompleteJs) or do I have to put the callback elsewhere?Ambassadress
after re-reading your edit, I understand that I no longer need to use ajaxCall, however, where do I use autoCompleteJs?Ambassadress
Does that answer your question?Eisenhower
Yes it does, I think I just need one more thing, which is pure javascript: how do I call this autocomplete as there does not seem to be any identifier. Finally, can you please provide the whole code to answer my question in order to have the code available for the next developer having this issue?Ambassadress
let us continue this discussion in chatEisenhower
A
2

With the help of Dave Whittaker, here is the solution I came with.

I had to change some behaviors to get:

  • the desired text (from autocomplete or not) in an ajaxText element
  • the possibility to have multiple autocomplete forms on same page
  • submit answer on ajaxText before blurring when something is selected in autocomplete suggestions.

Scala part

private def getSugggestions(current: String, limit: Int):List[String] = {
  /* returns list of suggestions */
}

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


def xml = {
  val id = "myId" //possibility to have multiple autocomplete fields on same page
  Script(OnLoad(JsRaw("jQuery('#"+id+"').createAutocompleteField("+autoCompleteJs.toJsCmd+")")))     ++
  SHtml.ajaxText(cell.get, s=>{ cell.set(s); SearchMenu.recomputeResults; Noop}, "id" -> id)
}

Script to insert into page header:

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

Note: I accepted Dave's answer, mine is just to provide a complete answer for my purpose

Ambassadress answered 17/4, 2012 at 8:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.