How to use a JS library like Awesomplete within a LiveView?

I’m trygint to get Awesomplete working to add multiple tags to a post.
However following instructions doesn’t do anything, i.e.:

  • I have a working app with Posts and Tags as liveview generated models.
  • My Post’s live form_component.ex looks like:
<%= f = form_for @changeset, "#",
  id: "post-form",
  phx_target: @myself,
  phx_change: "validate",
  phx_submit: "save" %>

  <%= label f, "Post title" %>
  <%= text_input f, :title %>
  <%= error_tag f, :title %>

## Tag input(Awesomplete multi-select)
## @tags = " " - this is set in the form_component.ex/apply_action(socket, :new, _params)
## @tag_names = "tagone, tagtwo, tagthree ..."

  <div class="form-group">
    <%= label f, :tags, class: "form-label" %>
    <%= text_input f, :tags, value: @tags, "data-list": @tag_names, "data-multiple": true %>
    <%= error_tag f, :tags %>
  </div>

  <%= submit "Save", phx_disable_with: "Saving..." %>
</form>

## Awesomplete multi-select script:
<script >
  new Awesomplete('input[data-multiple]', {
    filter: function(text, input) {
      return Awesomplete.FILTER_CONTAINS(text, input.match(/[^,]*$/)[0]);
    },
    item: function(text, input) {
      return Awesomplete.ITEM(text, input.match(/[^,]*$/)[0]);
    },
    replace: function(text) {
      var before = this.input.value.match(/^.+,\s*|/)[0];
      this.input.value = before + text + ", ";
    }
  });
</script>

Unfortunately, typing in the Tag input doesn’t display any auto-suggest with tags.
Anyone has ideas on how to get auto-suggest working without Liveview-specific client-server overhead?

Javascript interop in LiveView is done via Hooks, see the official docs for more details:

To handle custom client-side JavaScript when an element is added, updated, or removed by the server, a hook object may be provided with the following life-cycle callbacks

I did look into those docs, but still couldn’t figure the way to make it work with Awesomplete.
Can you point to some relevant example code?

The docs I linked have 2 code examples, one for a Phone number input and another for Infinit Scroll. Just adapt them to your use case.

So you need to do on the Phoenix Hook what you have inside <script>...</script>.

That didn’t work. '(
I updated my form_component as:

<%= text_input f, :tags, id: "tags-input", "phx-hook": "Awesomhook", 
value: @tags, "data-list": @tag_names, "data-multiple": true,  autocomplete: false %>
<%= error_tag f, :tags %>

and moved the JS code to app.js:

let Hooks = {}
Hooks.Awesomhook = {
  mounted() {
    new Awesomplete('input[data-multiple]', {
      filter: function(text, input) {
        return Awesomplete.FILTER_CONTAINS(text, input.match(/[^,]*$/)[0]);
      },
      item: function(text, input) {
        return Awesomplete.ITEM(text, input.match(/[^,]*$/)[0]);
      },
      replace: function(text) {
        var before = this.input.value.match(/^.+,\s*|/)[0];
        this.input.value = before + text + ", ";
      }
    });
  }
}
let liveSocket = new LiveSocket("/live", Socket, {
  params: {_csrf_token: csrfToken},
  hooks: Hooks
})

However nothing happens (no auto-suggest, no errors on client/server), except (as before) the liveview gets triggered when typing in the Tags input field.
What am I missing here?

Liveview does update your input tag each time you change the value. Try setting it to phx-update="ignore".

Also instead of 'input[data-multiple]' you should attach to this.el. Given that the liveview DOM can change partially you should attach js functionality on a per element basis not globally. You can’t just once apply awesomplete to all your input, but you need to attach it to each input when it comes up and likely also destroy the instance when the input it removed from the page.

1 Like

Thanks for pointers! But the problem persists, i.e. I see no auto-suggestions.

I changed the relevant line to

mounted() {
  new Awesomplete(this.el, { .... })

Also I don’t quite understand why on first page/form load the generated markup is following:

<div class="awesomplete">
  <input class="form-input" data-list="tag1, tag2, tag3" id="tags-input" name="post[tags]" 
    phx-hook="Awesomhook" phx-update="ignore"  type="text" value="" data-multiple="" 
    autocomplete="off" aria-expanded="false" aria-owns="awesomplete_list_1" role="combobox">

  <ul hidden="" role="listbox" id="awesomplete_list_1"></ul>

  <span class="visually-hidden" role="status" aria-live="assertive" aria-atomic="true">
    Type 2 or more characters for results.</span>
</div>

and after typing a single letter the wrapping <div class="awesomplete"> and ul, span vanish leaving only:

<input class="form-input" data-list="tag1, tag2, tag3" id="tags-input" name="post[tags]" 
  phx-hook="Awesomhook" phx-update="ignore"  type="text" value="" data-multiple
  aria-expanded="false"> 

What am I doing wrong?

What happens is that once you start typing, LiveView will kick in and any markup added there by the JS library will vanish on re-renders.

Surround your input tag in a div with phx-update="ignore" as suggested above. Adding this on the input itself doesn’t help in this case, since Awesomeplete initializes in an html node that is a sibling to the input.

Already tried that too. But weirdly, looks like Liveview doesn’t respect that.
I still see the <div class="awesomplete"> et al. being wiped out.

Make sure you are also adding an ID to the div with phx-update="ignore".

When using phx-update , a unique DOM ID must always be set in the container. If using “append” or “prepend”, a DOM ID must also be set for each child.

4 Likes

Ah, thank you, missed this one and it did the trick!
So, the Awesomplete library is working as expected now.

1 Like