Text Input widget that remembers what previous entries

I want a dropdown of previous entries.

Me too!

1 Like

I am not sure exactly what you are requesting. But you can add new feature requests on Github Issues · holoviz/panel (github.com)

Sometimes text boxes have dropdowns of previously entered values (cached)

1 Like

You can check it the next link

https://www.w3schools.com/howto/howto_js_autocomplete.asp

I tried it to adapt it with the reactiveHTML custom component as shown below and some questions arise to me I do not know how to solve it, for example. how to add a variable in the component scope to remember the entered values accesible from the different scripts. In the example below I use the global window scope, but the problem with this is you can have only one custom input. A hacky solution would be to add a random variable to the global window scope, but i did not implement yet. I am still struggling with some problems in the callbacks, and the keyup and keydown events. Nonetheless, here below is a first draft for a remember input.

Edit: the widget autocompleteInput with restrict = False and every time a new value is entered, add it to the options isn’t it what you want?

import panel as pn, param
from panel.reactive import ReactiveHTML

css = """
* {
  box-sizing: border-box;
}

body {
  font: 16px Arial;  
}

/*the container must be positioned relative:*/
.autocomplete {
  position: relative;
  display: inline-block;
}

input {
  border: 1px solid transparent;
  background-color: #f1f1f1;
  padding: 10px;
  font-size: 16px;
}

input[type=text] {
  background-color: #f1f1f1;
  width: 100%;
}

input[type=submit] {
  background-color: DodgerBlue;
  color: #fff;
  cursor: pointer;
}

.autocomplete-items {
  position: absolute;
  border: 1px solid #d4d4d4;
  border-bottom: none;
  border-top: none;
  z-index: 99;
  /*position the autocomplete items to be the same width as the container:*/
  top: 100%;
  left: 0;
  right: 0;
}

.autocomplete-items div {
  padding: 10px;
  cursor: pointer;
  background-color: #fff; 
  border-bottom: 1px solid #d4d4d4; 
}

/*when hovering an item:*/
.autocomplete-items div:hover {
  background-color: #e9e9e9; 
}

/*when navigating through the items using the arrow keys:*/
.autocomplete-active {
  background-color: DodgerBlue !important; 
  color: #ffffff; 
}
"""
pn.extension(raw_css=[css])

class history_input(ReactiveHTML):
    value = param.String('')
 #   onkeydown="${script('keyDown')}"
    _template = """
    <form autocomplete="off">
  <div class="autocomplete" style="width:300px;">
    <input id="myInput" type="text" name="myCountry" placeholder="Country" 
         oninput="${script('input')}"
         value="${value}"
         >
    </input>
  </div>
</form>
"""    
    _scripts = {
     'render': """ window.countries = [] ;
      document.addEventListener("click", function (e) {
      self.closeAllLists(e.target);
  });
     """,
     'input': """
        var a, b, i, val = myInput.value;
        self.closeAllLists();
        if (!val) { return false;}
        currentFocus = -1;
        a = document.createElement("DIV");
        a.setAttribute("id", myInput.id + "autocomplete-list");
        a.setAttribute("class", "autocomplete-items");
        myInput.parentNode.appendChild(a);
        for (i = 0; i < countries.length; i++){
            if (countries[i].substr(0, val.length).toUpperCase() == val.toUpperCase()) {
                b = document.createElement("DIV");
                b.innerHTML = "<strong>" + countries[i].substr(0, val.length) + "</strong>";
                b.innerHTML += countries[i].substr(val.length);
                b.innerHTML += "<input type='hidden' value='" + countries[i] + "'>";
                b.addEventListener("click", function(e) {
                myInput.value = this.getElementsByTagName("input")[0].value;
                self.closeAllLists();
            });
            a.appendChild(b);
            }
        }

        myInput.addEventListener("keydown", function(e) {
            var x = document.getElementById(myInput.id + "autocomplete-list");
            if (x) x = x.getElementsByTagName("div");
            if (e.keyCode == 40) {
                currentFocus++;
                console.log(currentFocus,'up', x.length)
                addActive(x);
            } else if (e.keyCode == 38) {                 
                currentFocus--;
                console.log(currentFocus,'down', x.length)
                addActive(x);
            } else if (e.keyCode == 13) {
                 e.preventDefault();
                if (myInput.value.length>0 && currentFocus<0) {countries.push(myInput.value); };
                countries = countries.filter(x => x !== null)
                if (x && x.length>0){console.log(x[currentFocus])}
                if (currentFocus > -1) {
                if (x) x[currentFocus].click();
                }
                myInput.value = '';
            }
        });

        function addActive(x) {
            if (!x) return false;
            removeActive(x);
            if (currentFocus >= x.length) currentFocus = 0;
            if (currentFocus < 0) currentFocus = (x.length - 1);
                x[currentFocus].classList.add("autocomplete-active");
        }

        function removeActive(x) {
            for (var i = 0; i < x.length; i++) {
            x[i].classList.remove("autocomplete-active");
            }
        }
        """,   
     'closeAllLists':"""
            var x = document.getElementsByClassName("autocomplete-items");
            for (var i = 0; i < x.length; i++) {
            if (myInput != x[i]) {
                x[i].parentNode.removeChild(x[i]);
            }
            }
     """
   }


custom_input = history_input()

text = pn.pane.Str('')

tmpl = pn.template.VanillaTemplate(title='Remember Input')
tmpl.main.append(pn.Column(custom_input, text))
tmpl.servable()
1 Like

Can it persist after each refreshed session?

I think so, if the country array values are read from window.localStorage, but I never use it .

In anycase, the autocomplete widget with restrict=False is very much easier to use it. You can have a global dictionary (or use pn.state) to handle users, or sessions. Again, these kind of things, how to manage users and sessions I did not learn it well to use them, and I think good examples or tutorials is need in the user guide.

import panel as pn, param
from panel.reactive import ReactiveHTML

autocomplete = pn.widgets.AutocompleteInput(
    name='Autocomplete Input', options=['Physics'],
    restrict=False, placeholder='Write something here')

@pn.depends(autocomplete, watch=True)
def update_autocomplete(val):
    if val != '':  print (val, autocomplete.options)
    if val not in autocomplete.options and val != '':
        autocomplete.options = [*autocomplete.options, val]
        print ('added')

tmpl = pn.template.VanillaTemplate(title='Remember Input')
tmpl.main.append(pn.Column(autocomplete))
tmpl.servable()
1 Like

Using window.localStorage['countries], JSON.stringify and JSON.parse it works. You need the JSON functions because the local storage saves strings.

import panel as pn, param
from panel.reactive import ReactiveHTML

css = """
* {
  box-sizing: border-box;
}

body {
  font: 16px Arial;  
}

/*the container must be positioned relative:*/
.autocomplete {
  position: relative;
  display: inline-block;
}

input {
  border: 1px solid transparent;
  background-color: #f1f1f1;
  padding: 10px;
  font-size: 16px;
}

input[type=text] {
  background-color: #f1f1f1;
  width: 100%;
}

input[type=submit] {
  background-color: DodgerBlue;
  color: #fff;
  cursor: pointer;
}

.autocomplete-items {
  position: absolute;
  border: 1px solid #d4d4d4;
  border-bottom: none;
  border-top: none;
  z-index: 99;
  /*position the autocomplete items to be the same width as the container:*/
  top: 100%;
  left: 0;
  right: 0;
}

.autocomplete-items div {
  padding: 10px;
  cursor: pointer;
  background-color: #fff; 
  border-bottom: 1px solid #d4d4d4; 
}

/*when hovering an item:*/
.autocomplete-items div:hover {
  background-color: #e9e9e9; 
}

/*when navigating through the items using the arrow keys:*/
.autocomplete-active {
  background-color: DodgerBlue !important; 
  color: #ffffff; 
}
"""
pn.extension(raw_css=[css])

class history_input(ReactiveHTML):
    value = param.String('')
 #   onkeydown="${script('keyDown')}"
    _template = """
    <form autocomplete="off">
  <div class="autocomplete" style="width:300px;">
    <input id="myInput" type="text" name="myCountry" placeholder="Country" 
         oninput="${script('input')}"
         value="${value}"
         >
    </input>
  </div>
</form>
"""    
    _scripts = {
     'render': """ window.countries = [] ;
    if (window.localStorage['countries']){
          countries = JSON.parse(window.localStorage['countries']);
          console.log('windows stogra',window.localStorage['countries'])
          }
      document.addEventListener("click", function (e) {
      self.closeAllLists(e.target); 
  });
     """,
     'input': """
        var a, b, i, val = myInput.value;
        self.closeAllLists();
        if (!val) { return false;}
        currentFocus = -1;
        a = document.createElement("DIV");
        a.setAttribute("id", myInput.id + "autocomplete-list");
        a.setAttribute("class", "autocomplete-items");
        myInput.parentNode.appendChild(a);
        for (i = 0; i < countries.length; i++){
            if (countries[i].substr(0, val.length).toUpperCase() == val.toUpperCase()) {
                b = document.createElement("DIV");
                b.innerHTML = "<strong>" + countries[i].substr(0, val.length) + "</strong>";
                b.innerHTML += countries[i].substr(val.length);
                b.innerHTML += "<input type='hidden' value='" + countries[i] + "'>";
                b.addEventListener("click", function(e) {
                myInput.value = this.getElementsByTagName("input")[0].value;
                self.closeAllLists();
            });
            a.appendChild(b);
            }
        }

        myInput.addEventListener("keydown", function(e) {
            var x = document.getElementById(myInput.id + "autocomplete-list");
            if (x) x = x.getElementsByTagName("div");
            if (e.keyCode == 40) {
                currentFocus++;
                console.log(currentFocus,'up', x.length)
                addActive(x);
            } else if (e.keyCode == 38) {                 
                currentFocus--;
                console.log(currentFocus,'down', x.length)
                addActive(x);
            } else if (e.keyCode == 13) {
                 e.preventDefault();
                if (myInput.value.length>0 && currentFocus<0) {
                    countries.push(myInput.value); 
                    window.localStorage['countries'] = JSON.stringify(countries);
                };
                countries = countries.filter(x => x !== null)
                if (x && x.length>0){console.log(x[currentFocus])}
                if (currentFocus > -1) {
                if (x) x[currentFocus].click();
                }
                myInput.value = '';
            }
        });

        function addActive(x) {
            if (!x) return false;
            removeActive(x);
            if (currentFocus >= x.length) currentFocus = 0;
            if (currentFocus < 0) currentFocus = (x.length - 1);
                x[currentFocus].classList.add("autocomplete-active");
        }

        function removeActive(x) {
            for (var i = 0; i < x.length; i++) {
            x[i].classList.remove("autocomplete-active");
            }
        }
        """,   
     'closeAllLists':"""
            var x = document.getElementsByClassName("autocomplete-items");
            for (var i = 0; i < x.length; i++) {
            if (myInput != x[i]) {
                x[i].parentNode.removeChild(x[i]);
            }
            }
     """
   }


custom_input = history_input()

text = pn.pane.Str('')

tmpl = pn.template.VanillaTemplate(title='Remember Input')
tmpl.main.append(pn.Column(custom_input, text))
tmpl.servable()
1 Like