Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Awesomplete via channel playground

awesomplete_via_channel_playground.livemd

Awesomplete via channel playground

Mix.install([
  {:kino, "~> 0.14"},
  {:phoenix_html, "~> 4.0"},
  {:phoenix_form_awesomplete, "~> 1.0.0"},
  {:kino_explorer, "~> 0.1.11"},
  {:nimble_csv, "~> 1.2"}
])

Introduction

This Livebook is meant to test out Awesomplete (PhoenixFormAwesomplete).

In this livebook the ajax requests are replaced with events via the Phoenix channel (= live socket).

About Awesomplete

PhoenixFormAwesomplete is a Phoenix form helper that utilizes Lea Verou’s autocomplete / autosuggest / typeahead / inputsearch Awesomplete widget.

It comes with an AwesompleteUtil javascript library which adds the following features:

  • Dynamic remote data loading; based on what is typed-in it performs an ajax lookup.
  • Allow HTML markup in the shown items. Show value with description. Optionally search in the description text.
  • Show when there is an exact match.
  • Show when there isn’t a match.
  • When there is an exact match show related data (supplied in the remote data) in other parts of the page.
  • Select the highlighted item with the tab-key.

Online demo

Playground setup

For the data, a csv file with product categories is used. This file is parsed and converted to a list. There are three columns in the csv: name, code and description.

# productcat.csv does not contain column headers
product_category_list =
  Kino.FS.file_path("productcat.csv")
  |> File.stream!()
  |> NimbleCSV.RFC4180.parse_stream(skip_headers: false)
  |> Stream.map(fn [name, code, descr] ->
    %{name: name, code: code, description: descr}
  end)
  |> Enum.to_list()

The content of the csv file can be examined below

df =
  Kino.FS.file_path("productcat.csv")
  |> Explorer.DataFrame.from_csv!(header: false)

Cache the product category list in ets.

# create ets table
if Enum.member?(:ets.all(), :my_lb_cache) == false do
  :ets.new(:my_lb_cache, [:set, :public, :named_table])
end

:ets.insert(:my_lb_cache, {"product_category_list", product_category_list})

Playground

The code below will run the autocomplete functionality.

Try the different options in the mytest-autocomplete span tag, and reevaluate to see the effect.

defmodule KinoDocs.Awesomplete do
  use Kino.JS
  use Kino.JS.Live

  def new() do
    Kino.JS.Live.new(
      __MODULE__,
      """
      
        
          Product category
          
        

        

        Clear
      
      """
    )
  end

  def get_product_category_list() do
    # get it from the ets cache
    key = "product_category_list"
    [{^key, value}] = :ets.lookup(:my_lb_cache, key)
    value
  end

  def safe_downcase(text) do
    if is_nil(text), do: nil, else: String.downcase(text)
  end

  def filter_product_category(search_phrase) do
    search = safe_downcase(search_phrase)
    product_category_list = get_product_category_list()

    filter =
      fn rec ->
        String.contains?(String.downcase(rec.name), search) or
          String.contains?(String.downcase(rec.description), search)
      end

    Enum.filter(product_category_list, filter)
  end

  @impl true
  def init(html, ctx) do
    {:ok, assign(ctx, html: html)}
  end

  @impl true
  def handle_connect(ctx) do
    {:ok, ctx.assigns.html, ctx}
  end

  @impl true
  def handle_event("update-prodcat-list", %{"value" => value, "id" => id}, ctx) do
    list = filter_product_category(value)
    broadcast_event(ctx, "update-list-#{id}", %{searchPhrase: value, searchResult: list})
    {:noreply, ctx}
  end

  asset "main.js" do
    """
    import { Awesomplete, AwesompleteUtil, attachAwesomplete, copyValueToId } from 'https://nico-amsterdam.github.io/awesomplete-util/js/awesomplete_bundle.min.mjs'

    export function init(ctx, html) {
      ctx.importCSS("https://nico-amsterdam.github.io/awesomplete-util/css/awesomplete_bundle.css").then(() => 
      {
         console.log('Loaded awesomplete styling assets')
      })
      ctx.importCSS("main.css")
      ctx.root.innerHTML = html

      function ajax2live(url, urlEnd, val, fn, xhr) {
        if (url && url.startsWith('livesocket:')) {
            const awe = this
                , phxEvent = url.substr(url.indexOf(':') + 1)
                , phxData = {'value':val, 'id':awe.input.id}
            
            console.log('request ' + phxEvent + ' "' + val + '"')
            ctx.pushEvent(phxEvent, phxData)
        }
        else
        {
            AwesompleteUtil.ajax(url, urlEnd, val, fn, xhr)
        }
      }
          
      const AU = AwesompleteUtil,
            customAwesompleteContext = 
      {

        // These functions and objects can be referenced by name in mytest-autocomplete 
        // This list can be customized.

          filterContains:   AU.filterContains
        , filterStartsWith: AU.filterStartsWith
        , filterWords:      AU.filterWords
        , filterOff:        AU.filterOff

        , item:             AU.item          // does NOT mark matching text
        , itemContains:     AU.itemContains  // this is the default, no need to specify it.
        , itemStartsWith:   AU.itemStartsWith
        , itemMarkAll:      AU.itemMarkAll   // also mark matching text inside the description
        , itemWords:        AU.itemWords     // mark matching words

        , jsonFlatten:      AU.jsonFlatten   // convertResponse utility to flatten JSON

        // add your custom functions and/or lists here

        , ajax2live:        ajax2live

      }
      
      self.attachWhenFocus = (e) => {
        if (!e.classList.contains('touched')) {
          e.classList.add('touched')

          // search element with the autocomplete settings 
          const hookElem = document.getElementById('mytest-autocomplete')

          const awe = attachAwesomplete(hookElem
                                    , customAwesompleteContext
                                    , {} /* defaultSettings */ )

          ctx.handleEvent("update-list-" + awe.input.id, ({searchResult, searchPhrase}) => {
             console.log('response for "' + searchPhrase + '" has ' + searchResult.length + ' items')
             AwesompleteUtil.updateList(awe, searchResult, searchPhrase)
          })

        }
        e.focus()
      }
    }
    """
  end

  asset "main.css" do
    """
    input.autocomplete:focus-visible {
      outline: unset
    }
    """
  end
end

KinoDocs.Awesomplete.new()

Attribute list

Options for the mytest-autocomplete span tag:

  • ajax - Replace ajax function. Supplied function receives these parameters: (url, urlEnd, val, fn, xhr). fn is the callback function. Default: AwesompleteUtil.ajax.
  • assign - Put the Awesomplete object in the customAwesompleteContext. true/false/name. If true the key will be ‘awe_’ + id of the input tag. Default: false
  • autoFirst - true/false. Automatically select the first element. Default: false.
  • combobox - Id of the combobox button. true/false/id. If true the assumed button id is ‘awe_btn_’ + id of the input tag. Default: false
  • container - Container function as defined in Awesomplete. By default a div element is added as the parent of the input element.
  • convertInput - Convert input function which receives the input text as parameter. This function is used normalize the input text. Internally convert the input text for search calls and for comparison with the suggestions. By default it trims the input and converts it to lowercase for a case-insensitive comparison. It is applied to both the input text and the suggestion text before comparing. In advanced cases like the multiple values, the convertInput is used to extract the search text.
  • convertResponse - Convert JSON response from ajax calls. This function is called with the parsed JSON, and allows conversion of the data before further processing. Default: nil - no conversion.
  • data - Data function as defined in Awesomplete
  • debounce - Time in milliseconds to wait for additional user input before doing the ajax call to retrieve suggestions. It limits the rate at which the json service is called per user session.
  • descr - Name of the field in the data list (the JSON response) that contains the description text to show below the value in the suggestion list. Default: no description
  • descrSearch - true/false. Filter must also search the input value in the description field. Default: false
  • filter - Filter function as defined in Awesomplete. Mostly use Awesomplete.FILTER_STARTSWITH or Awesomplete.FILTER_CONTAINS. If label is different as value, filter on value with AweompleteUtil.filterStartsWith, AwesompleteUtil.filterContains or AwesompleteUtil.filterWords. To turn off filtering, use AwesompleteUtil.filterOff.
  • item - Item function as defined in Awesomplete with parameters text, input and itemId. Default is to highlight all occurrences of the input text. Use AwesompleteUtil.itemStartsWith or AwesompleteUtil.itemWords if that matches with the used filter.
  • label - Name of the field in the data list (the JSON response) that contains the text that should be shown instead of the value.
  • list - Data list as defined in Awesomplete.
  • listLabel - Denotes a label to be used as aria-label on the generated autocomplete list.
  • loadall - true/false. Use true if the data list contains all items, and the input value should not be used in ajax calls. Default: false
  • limit - number. If a limit is specified, and the number of items returned by the server is equal or more as this limit, the AwesompleteUtil code assumes that there are more results, so it will re-query if more characters are typed to get more refined results. The limit:1 tells that not more than 1 result is expected, so the json service doesn’t have to return an array. With limit:0 it will always re-query if more characters are typed and the result doesn’t have to be an array either. Limit:-1 will always requery and the expected result is an array. When no limit is specified, the code assumes that all possible suggestions are returned based on the typed characters, and it will not re-query if more characters are typed. It uses the specified filter for the suggestions in the dropdown. Default: no limit
  • maxItems - Maximum number of suggestions to display. Default: 10
  • minChars - Minimum characters the user has to type before the autocomplete popup shows up. Default: 2
  • multiple - true/false/characters. Separators to allow multiple values. If true, the separator will be the space character. Default: false
  • prepop - true/false. If true do lookup initial/autofilled value and send awesomplete-prepop event. Default: false
  • replace - Replace function as defined in Awesomplete. The replace function will be called for suggestions, to determine whether the input text matches a suggestion after replacement.
  • sort - Sort function as defined in Awesomplete
  • statusNoResults - Screen reader text to replace the default: ‘No results found’
  • statusTypeXChar - Screen reader text to replace the default: ‘Type {0} or more characters for results’. The placeholder {0} will be replaced with the minimum number of characters (minChars).
  • statusXResults - Screen reader text to replace the default: ‘{0} results found’. The placeholder {0} will be replaced with the number of results.
  • url - url for ajax calls.
  • urlEnd - Addition at the end of the url for the ajax call, after the input value. Or a function, which receives the value and must return the last part of the url.
  • value - Name of the field in the data list (the JSON response) that contains the value.