Setting hidden datalist option values
Asked Answered
P

5

29

In the snippet below, I have two methods to choose an item: input with datalist and traditional select with options.

The select element keeps the option values hidden, and we're still able to get it with this.value. However, with the datalist, the value is actually displayed and the text content of the option is displayed as a secondary label.

What I'd like is to have the input+datalist approach behave like a traditional select, where "Foo" and "Bar" are shown as options that when selected have values of "1" and "2" respectively.

I've also added a repeated name "Foo" with value "3". This is to show that any solution must not depend on unique options.

<input list="options" onchange="console.log(this.value)"/>
<datalist id="options">
  <option value="1">Foo</option>
  <option value="2">Bar</option>
  <option value="3">Foo</option>
</datalist>

<select onchange="console.log(this.value)">
  <option value=""></option>
  <option value="1">Foo</option>
  <option value="2">Bar</option>
  <option value="3">Foo</option>
</select>
Prebend answered 22/8, 2016 at 18:32 Comment(5)
I think this link answers what you want how-to-display-the-text-in-datalist-html5-and-not-valueItemized
@Itemized thanks, I found that post too but unfortunately it only supports unique display values. For example, if another option in my example were "Foo" with value "3", selecting it would produce a value of "1" instead of "3".Prebend
What should happen if the user types 1, should the text become Foo? Should .value return 1 or 3 if the user types Foo manually?Planetstruck
@Planetstruck I'd like for the numeric values to be transparent to the user. Only Foo, Bar, etc should be accessible as suggestions - just like a traditional <select>. If the user types Foo there will be two suggestions and the value should correspond with the selected value (when clicked or highlighted with keyboard). If no suggestions are selected and the text value is Foo we can only assume the user selected the first option.Prebend
I have the same issue in a VueJS project. Initially I thought datalist was a VueJS feature. I just linked this question to help other users: #60004948Verified
S
10

You can use data-value and jquery to make your value hidden.

e.g:

$(document).ready(function() {

    $('#submit').click(function()
    {
        var value = $('#selected').val();
        alert($('#browsers [value="' + value + '"]').data('value'));
    });
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<input id="selected" list="browsers" name="browser">
<datalist id="browsers">
    <option data-value="1" value="InternetExplorer"></option>
    <option data-value="2" value="Firefox"></option>
    <option data-value="3" value="Chrome"></option>

</datalist>
<input id="submit" type="submit">

jsfiddle

Thanks to @guest271314

Semipermeable answered 3/1, 2018 at 11:15 Comment(0)
P
7

There is no native way. For text inputs

The input element represents a one line plain text edit control for the element's value.

So it's not possible to make a text input display some text different than its value.

You could hijack the value getter in HTMLInputElement.prototype, but I don't recommend it, and I don't see any way to know which option was chosen if the values are not unique.

const des = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
Object.defineProperty(HTMLInputElement.prototype, 'value', {
  get: function() {
    const value = des.get.call(this);
  
    if (this.type === 'text' && this.list) {
      const opt = [].find.call(this.list.options, o => o.value === value);
      return opt ? opt.dataset.value : value;
    }

    return value;  
  } 
});
<input list="options" oninput="console.log(this.value);" />
<datalist id="options">
  <option data-value="1">Foo</option>
  <option data-value="2">Bar</option>
  <option data-value="3">Foo</option>
</datalist>

Or maybe you can let the input show the value of the option instead of the text, but replace it immediately:

document.querySelector('input').addEventListener('input', event => {
  const value = event.target.value;
  const opt = [].find.call(event.target.list.options, o => o.value === value);

  if (opt) {
    event.target.value = opt.textContent;
  }
});
<input list="options" oninput="console.log(this.value);" />
<datalist id="options">
  <option value="1">Foo</option>
  <option value="2">Bar</option>
  <option value="3">Foo</option>
</datalist>

In the second example, oninput="console.log(this.value)" shows the value of the option because that code runs before replacing the value to the text content. If you want later .value accessions to return the option value, you will need to hijack the value getter like in the first example.

Planetstruck answered 5/9, 2016 at 23:27 Comment(1)
Thanks, although hacky I appreciate the solutions. In the first approach however, it doesn't seem possible to get a value of 3.Prebend
P
1

The correct syntax of datalist is like bellow. Also there is no point to have two options with the same name. I took the JavaScript out of the HTML as it should be. ID attribute of the individual options can be replaced with other attribute.

$(function() {
  $('input[name=chooseOption]').on('input',function() {
    var selectedOption = $('option[value="'+$(this).val()+'"]');
    console.log(selectedOption.length ? selectedOption.attr('id') : 'This opiton is not in the list!');
  });
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<input list="options" name="chooseOption">
<datalist id="options">
  <option id="1" value="Foo">
  <option id="2" value="Bar">
  <option id="3" value="Foo">
</datalist>
Pinky answered 12/9, 2016 at 9:26 Comment(0)
T
1

So for the codebase I'm working on, I have a programatic fieldWriter for writing dynamic forms. I was asked to turn a bunch of selects into autocomplete datalists. What I did for this is set up 3 elements.

  • One datalist with the name "somefield_dl"
  • One visible input with the name "somefield_proxy"
  • One hidden input with the name "somefield"

On the datalists, I set each option like <option data-value="439" value="Readable Text"></option>

With event handlers, anytime the "somefield_proxy" changes, the "somefield" is changed to the data-value of the option from "somefield_dl" with a matching value.

Code-wise, for using these, the main difference is that every time you update the field's value, you have to trigger the click event on the hidden input to update the proxy input. Also, when gathering data, you have to exclude the elements whose name ends with _proxy.

Tune answered 26/6, 2019 at 15:54 Comment(0)
M
1

If you can turn the "value" associated with the option into a number, you can use unicode invisible characters, U+E0001 through U+E007F to store the "value" part of the option alongside the title.

Append an arbitrary number of invisible characters to the end of your title. Since there are ~120 such invisible characters, each double-byte invisible character on the end of a title represents an integer from 0 to 120.

My solution below stores breaks up numbers into equal chunks, so that number parts stored in invisible characters need only be summed as is to get the original. If numbers could be broken up such that each invisible character represents increasingly larger portions of the original number, perhaps using powers of two or some other exponential math/magic, then the solution would be significantly faster.

const BASE_CODEPOINT = 'E0001';
const NUM_EMPTY_CHARS = 120; // actually 127, but let's be safe.

const numToEmptyChar = num => String.fromCodePoint(`0x${(parseInt(BASE_CODEPOINT, 16) + num).toString(16)}`);

const withNumberAsTrailingEmptySpace = (string, number, maxNumber = NUM_EMPTY_CHARS) => {
const numChars = Math.ceil(maxNumber / NUM_EMPTY_CHARS);
const numCharsMinusOne = Math.max(0, numChars - 1);

return string
    + numToEmptyChar(NUM_EMPTY_CHARS).repeat(numCharsMinusOne)
    + numToEmptyChar(number - (NUM_EMPTY_CHARS * numCharsMinusOne));
};

const numberFromTrailingEmptySpace = (string, maxNumber = NUM_EMPTY_CHARS) => {
const numChars = Math.ceil(maxNumber / NUM_EMPTY_CHARS);

let total = 0;
for (let i = 0; i < numChars; i++) {
    const codePoint = string.codePointAt(string.length - 2 * (i + 1));
    total += codePoint - parseInt(BASE_CODEPOINT, 16);
}
return total;
};

let mystring = "Anything at all can go here.....";

let maxNum = 2000;
let number = 1337;
let withNumber = withNumberAsTrailingEmptySpace(mystring, number, maxNum);
console.log(numberFromTrailingEmptySpace(withNumber, maxNum));

So, you'd end up creating your datalist via JS (untested):

const options = [
    {label: "Foo", value: 1},
    {label: "Bar", value: 2},
    {label: "Baz", value: 3}
];

const input = document.createElement('input');
input.setAttribute('list', 'options');
input.addEventListener('change', e => {
    // https://w3c.github.io/input-events/#interface-InputEvent-Attributes
    const isDatalist = (typeof inputType === "undefined") || (inputType === "insertReplacementText");
    if (isDatalist) {
        const idx = numberFromTrailingEmptySpace(e.target.value, options.length);
        const selectedOption = options[idx];
        e.target.value = selectedOption.label;

        console.log("The user selected option: ", selectedOption);
    }
});

const datalist = document.createElement('datalist');
datalist.setAttribute('id', 'options');

options.forEach(({label}, idx) => {
    const o = document.createElement('option');
    o.label = withNumberAsTrailingEmptySpace(label, idx, options.length);
    datalist.appendChild(o);
});
Muns answered 16/5 at 22:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.