Autocomplete searchable list select one

Hello,

What is the easiest way to create a select_one that can autocomplete, instantly searchable with ODK-X? I’m thinking of in HTML.

Thank’s

Hi @Patrick30!

I am not quite sure what you mean about a select_one that can autocomplete. Select_one has a defined set of options, so do you mean something like, if the select_one were for state names, if you start tying M it would give you all the M states?

There may be some examples of how to do this (or something similar) in the reference apps; here’s an example with the Hope Study where you can search for a patient ID:
https://docs.odk-x.org/tables-sample-app-hope/?highlight=search#integrate-with-survey
The cold chain demo lets you search by refrigerator ID or type:
https://docs.odk-x.org/cold-chain-tour-refrigerator-types/?highlight=search

Downloading and testing those to see if they do what you want and then looking at their underlying code and files may work.

@Patrick30. You are correct that you can add any prompt you want using html/javascript. You might want to look at how the handlebars template system works.

Here is some documentation: https://docs.odk-x.org/xlsx-converter-using/#customizing-prompts

NOTE: By providing a customPromptTypes.js file in your form directory, you can define Backbone views that extend the base prompts.

Here is a previous post on example creating a custom prompt type: Problem with customPrompt in survey

Hello,

I created a customPromptTypes.js and integrated the jquery-ui autocomplete widgets. It works, except that I can’t call my values_list. Can you give me a hand.

define([‘promptTypes’, ‘jquery’, ‘underscore’, ‘prompts’],
function(promptTypes, $, _, prompts) {
return {
$(’#saisiauto’).autocomplete(
{
source : villes
})
}
};

Besides, I have the same problem as AlbertoB whose Survey application crashes after pushing it into my tablet since I created a prompt_types.

1 Like

Hello,

Have you tried to connect Chrome remote debugger? If you connect your tablet (and have enabled USB debugging in android settings), you can open chrome and go to: chrome://inspect/#devices

There you should be able to select the web view running in survey. Typically errors are shown in the javascript console, which can help narrow down the problem… I will be happy to assist if you post the errors here or if you get stuck in the process :wink:

/emil

1 Like

Hello Emil,

thank you for your help

I fixed the problem with Survey. Now it doesn’t crash anymore.

however, I am still unable to return my “villes” values_list inside the Jquery UI autocomplete widgets. I understand the logic of the Jquery widgets that you can find here: https://jqueryui.com/autocomplete/ and also understand the logic of the ODK-X prompt_types but I don’t know how to nest the two or, at least, call my variables contained in values_list. Note that I do well in HTML / CSS but that I have basic knowledge of javascript. Here is my code again

define([‘promptTypes’, ‘jquery’, ‘underscore’, ‘prompts’],
function(promptTypes, $, _, prompts) {
return {
$(’#saisiauto’).autocomplete(
{
source : villes
});
} );

“saisiauto” it’s the name of type in my survey sheet.

I think you might have a couple issues there. The first is that autocomplete doesn’t actually come with jquery out of the box, but instead you would need to be importing the jquery-ui library. I think this can be done by dropping files downloaded from the site you shared into the app/system/libs folder and adding imports to app/system/survey/js/main.js. I’m not sure if these would be synced or not, so worth double checking across devices.
Edit 2020-07-15 - This won’t work on devices, see thread below

The second issue is accessing and formatting the data from your choices list. I’m not sure what the recommended way to go about this was, but I had some success using the renderContext available to the custom prompt type. Here’s my working custom prompt:

define(["promptTypes", "jquery", "jqueryUi"], function (promptTypes, $) {
    return {
        custom_select_autocomplete_jquery: promptTypes.input_type.extend({
            afterRender: function () {
                try {
                    const promptId = this.getPromptId();
                    const name = this.renderContext.name;
                    const columns = this.renderContext.metadata.kvMap.Column;
                    const choices = columns[name]._displayChoicesList;
                    const autocompleteOptions = choices.map(function (c) {
                        return {
                            value: c.data_value,
                            label: c.display.title.text,
                        };
                    });
                    // NOTE - requires jqueryUI lib import in app/system/survey/js/main.js
                    $("#slider-" + promptId).autocomplete({
                        source: autocompleteOptions,
                    });
                } catch (error) {
                    console.error(error);
                }
            },
        }),
    };
});

image
example for a list of countries

3 Likes

Another (possibly easier) alternative would be to just create a handlebars template to use the native datalist tag. If you define the field as select_one and a templatePath to the file, the following code should also work:

<form action="javascript:void(0);" onsubmit="odkLeaveField(this);">
    <label for="input-{{promptId}}">{{> labelHint}}</label>
    <input list="autocomplete-list" name="input-{{promptId}}" id="input-{{promptId}}">
    <datalist id="autocomplete-list">
        {{#each choices}}
        <option {{#if checked}} selected="selected" {{/if}} value="{{data_value}}">
            {{/each}}
    </datalist>
</form>

custom_select_autocomplete_datalist.handlebars

image
example output

Personally I think this way looks nicer and is easier to implement. The downside is that older devices may not support the tag. If I remember correctly, I think Android 4.4+ uses support from whatever version of google chrome is installed on the device (so as long as it’s reasonably updated then should work - https://caniuse.com/#feat=datalist ), but older android will likely not work.
There’s a bit more information about versions and updating here if useful: https://developer.chrome.com/multidevice/webview/overview

3 Likes

The system folder does not sync. It is installed by the APK and the APK may overwrite the system folder again if reset. The javascript in the system folder and the java code need to be match for the system to work so do NOT put code there.

Code for an application should reside in the “config” folder instead. The convention for javascript libraries is to put it in the assets folder. The config folder syncs to all devices.

More information can be found here:
https://docs.odk-x.org/app-designer-directories/

2 Likes

Thanks @W_Brunette, that’s really useful to know. I know how to import from config assets for tables HTML views, but is it possible to also import for a survey custom prompt?

With that in mind @Patrick30, I’d probably say try with datalist, or another option could be to use both a custom prompt and template possibly creating something in vanilla javascript (e.g. https://www.w3schools.com/howto/howto_js_autocomplete.asp), although I haven’t tried that myself.

1 Like

Hello Chrism
I opted for the handlebars. Autocompletion works great, but it doesn’t allow selection. Is there your side selection in the box? If yes, I will look my way.

Thank you

1 Like

Ah, yes my mistake. The input needs to be named differently so that odk knows how to extract and save the values. So I think the following should work to save the value:

<div id="container-{{promptId}}">
    <form action="javascript:void(0);" onsubmit="odkLeaveField(this);">
        <div class="form-group">
            <label for="slider-{{promptId}}">{{> labelHint}}</label>
            <div class="input-container">
                <input list="slider-{{promptId}}-list" id="slider-{{promptId}}" class="form-control" name="{{name}}"
                    value="{{lookup data name}}" tabindex="0" {{#eachProperty inputAttributes}} {{property}}="{{value}}"
                    {{/eachProperty}} {{#if disabled}} style="background-color:lightgray;" disabled="true" {{/if}}>
                <datalist id="slider-{{promptId}}-list">
                    {{#each choices}}
                    <option value={{data_value}}>{{display.title.text}}</option>
                    {{/each}}
                </datalist>
            </div>
        </div>
    </form>
</div>

The added challenge this brings however, is in the case where the data_value and display.title.text values are different in your choices. By default the datalist will show the value in the input box, but the user would likely want to see the label (a difference between datalists and selects). I think there’s probably a couple different ways around this, one thing I’ve tried in the past is having two different input fields, a main input with the datalist and label, and a second hidden input for the value which is sent to the database. You’d then need some extra scripts to keep the two in sync, demo code below:

<div id="container-{{promptId}}">
    <form action="javascript:void(0);" onsubmit="odkLeaveField(this);">
        <div class="form-group">
            <label for="input-{{promptId}}-list">{{> labelHint}}</label>
            <div class="input-container">
                {{!-- Main input box with datalist used to lookup values based on their display.title.text value --}}
                <input list="input-{{promptId}}-list" id="input-{{promptId}}-datalist"
                    onchange="handleOptionSelect(event);">
                <datalist id="input-{{promptId}}-list">
                    {{#each choices}}
                    <option data-data_value={{data_value}} data-display_title_text="{{display.title.text}}">
                        {{display.title.text}}</option>
                    {{/each}}
                </datalist>
                {{!-- Additional hidden input box used to track the corresponding data_value of selected option and populate to odk database --}}
                <input type="hidden" id="slider-{{promptId}}" class="form-control" name="{{name}}" tabindex="0"
                    {{#eachProperty inputAttributes}} {{property}}="{{value}}" {{/eachProperty}} {{#if disabled}}
                    style="background-color:lightgray;" disabled="true" {{/if}}>
            </div>
        </div>
    </form>
</div>

<script>
    var datalistInputEl = document.getElementById("input-{{promptId}}-datalist")
    var odkInputEl = document.getElementById("slider-{{promptId}}")
    var datalistEl = document.getElementById('input-{{promptId}}-list')
    initialiseValues()

    // on load lookup the data loaded (all form fields) and get the value for this specific question name.
    // populate that value into the input element, and lookup the corresponding display.title.text to input
    // into the datalist input
    function initialiseValues() {
        // handlebars lookup syntax used to get a specific variable property (name) from an object (data)
        var savedValue = "{{lookup data [name]}}"
        if (savedValue) {
            odkInputEl.value = savedValue
            const savedValueOption = datalistEl.querySelector('[data-data_value="' + savedValue + '"]');
            const savedValueText = savedValueOption.getAttribute('data-display_title_text')
            datalistInputEl.value = savedValueText
        }
    }

    // when an option is selected from the datalist find the corresponding option value and populate the odk input element
    function handleOptionSelect(e) {
        const selectedOptionValue = e.target.value
        const match = datalistEl.querySelector('[data-display_title_text="' + selectedOptionValue + '"]');
        const data_value = match.getAttribute('data-data_value');
        document.getElementById(id = "slider-{{promptId}}").value = data_value
    }
</script>

Let me know if you try either of these options and if they work for you.
Chris

1 Like

Thank you ! The second version works perfectly. However, it only works inside the Application Designer, but not when I push it to my tablet. The box appears but the list of values is not there.

1 Like

It’s working on my device, so possibly an issue with android/chrome/webview version as mentioned in one of my older messages.

What version of Android are you running and what version of google chrome do you have installed on the device?

The other way you could try to debug would be plug into your computer via USB, open google chrome and go to chrome://inspect/#devices in the URL bar. You should see a list of remote devices, one being the webview in odk survey. Click inpsect on that and you should be able to access console logs and errors from the console table in the window that pops up (although I’m not sure how much will be logged there).

1 Like

My tablet is very recent with the latest version of Android. But I’m wondering, why is the version of Chrome important since Survey is an application that doesn’t seem to use Chrome? I will try to see if I can solve the problem by myself, but if you have possible solutions, please let me know.

1 Like

Hi @Patrick30!

I don’t think @chrismclarke meant that Chrome was important for the tablet – it read to me like he was suggesting a way to check the error messages by plugging the tablet into a computer with chrome via USB, and then going to: chrome://inspect/#devices in the URL bar. Then his instructions were about how to see the errors. There are also the logs on the tablet that you can inspect.

Hello @chrismclarke @elmps2018

Here is the list of errors when I do the inspection.

1 Like

ODK survey uses webviews to display the survey, which is why you can use it both in application designer on your desktop browser and on a device. when running on device you essentially have a web browser inside the app.

Depending on android version this webview is handled in different ways, for android 4 it bundled with the system app, for 5-6 it is a standalone installed app (search the play store for webview), 7-9 it just uses the installed google chrome app (no installed webview app), Android 10 has now gone back to a standalone app (but pretty much an identical copy of chrome). So long story short, depending on your android version, the version of google chrome installed on the device may or may not impact the utility of ODK Survey.

I should emphasise though, that for most normal operation this impact is pretty much negligible - odk mostly just uses very standard components that have been around a very long time and full support for all browsers for the past 5-10 years. It is the datalist component that I’ve introduced which is not supported on older versions.

So it might be worth checking a) the version of android you are running (when you say latest does that mean 8/9/10?) and b) the version of google chrome installed (or just try update from the play store). If netiher of these work feel free to share your form with me and I can take a look or just provide my own example form.

2 Likes

The errors in the console look mostly harmless (due to the non-existing customStyles/promptTypes etc.), but I’m not so sure it should give a “404 not found” on the odkTables.js-file… @chrismclarke do you think it could be missing some essential files, or am I just reading the console output wrong?

1 Like

@chrismclarke It is possible to import custom libraries from assets folder but the libraries could easily have incompatibilities with the other libraries in the Survey framework. In main.js we use require to load the various libraries so you might be able to use require later but we have a config object and I’m not sure what it would do with the various library dependencies. The system was designed to load them all at beginning and if you put in competing libraries not using the config object (which you don’t have access to in config) I’m not sure what happens. If you experiment let us know the result. Also something to consider adding as a feature request based on results of exploration.

I would recommend (as you are saying) trying to make customization in pure JavaScript as it avoids the issues.

1 Like