I want a dropdown of previous entries.
Me too!
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)
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()
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()
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()