javajavafxautocompletecontrolsfx

Populating JavaFX ControlsFX Autocompletion Textfield resulting in duplicate content


I've been using the JavaFX ControlsFX TextFields.bindAutoCompletion() with asynchronous javafx tasks in order to populate autocompletion results from my neo4j database after a user enters two characters. The problem is that if the user clears out the text field and types new values to search, there are now two bindings, so two autocompletion popups show.

Shows a text field with two autocompletion bindings, one from the old input, and one from the current input. It should just show the top one.

I need to be able to completely unbind the textfield from the old list and bind it's autocompletion to the new list. It seems the abstract method i'm using, dispose() doesn't do anything in the standard AutoCompletionBinding class?

    AutoCompletionBinding<Client> clientBinding;
    private void getClientAutoComplete(TextField clientNameTextField) {
        String input = clientNameTextField.getText().toUpperCase();
        if (input.length() < 2  && clientBinding != null) {
            clientBinding.dispose();
        } else if (input.length() == 2) {
            var queryTask = SimpleCypher.getClientAutoComplete(input);

            queryTask.setOnSucceeded(event -> {
                AutoCompletionBinding<Client> clientBinding = TextFields.bindAutoCompletion(clientNameTextField, queryTask.getValue());
                clientBinding.setOnAutoCompleted(e -> getClientData(e.getCompletion().getId()));
            });

            // Start the task asynchronously
            Thread queryThread = new Thread(queryTask);
            queryThread.setDaemon(true); // Set as daemon thread to allow application exit
            queryThread.start();
        }
    }

Here is the Javafx Task:

    public static Task<List<Client>> getClientAutoComplete(String input){
        Task<List<Client>> task = new Task<>() {
                @Override
                protected List<Client> call() throws Exception {
                    List<Client> resultClients = new ArrayList<>();
                    try (Session session = DatabaseConnection.getSession()) {
                        Result result = session.run(
                                """
                                MATCH (n:Client)
                                WHERE toUpper(n.name) CONTAINS $textFieldInput
                                RETURN n.id AS id
                                , n.name AS name
                                , n.phone AS num
                                """,
                                Values.parameters("textFieldInput", input));
                        while (result.hasNext()) {
                            Record record = result.next();
                            resultClients.add(
                                new Client(
                                    record.get("id").asInt(),
                                    record.get("name").asString(),
                                    record.get("num").isNull() ? null : record.get("num").asString()
                            ));
                        }
                    }
                    return resultClients;
                }
            };
        task.setOnFailed(event -> SimpleCypher.handleQueryError(event));
        return task;
    }

I feel like the solution is to create my own custom class that overrides some of the abstract methods of AutoCompletionBinding. But what is the best way for me to implement this based on what i need, which is the ability for the user to type a value that is queried against the database and then populates the text field, while also removing any previous bindings from previous input?

Here is what I have so far for my implementation, but I'm not sure what all I have to actually put in the implementation to get it to work?:

import java.util.Collection;

import org.controlsfx.control.textfield.AutoCompletionBinding;

import javafx.scene.Node;
import javafx.util.Callback;
import javafx.util.StringConverter;

public class Neo4jAutoCompletionBinding<T> extends AutoCompletionBinding<T> {

    protected Neo4jAutoCompletionBinding(Node completionTarget,
            Callback<ISuggestionRequest, Collection<T>> suggestionProvider, StringConverter<T> converter) {
        super(completionTarget, suggestionProvider, converter);
        // TODO Auto-generated constructor stub
    }

    @Override
    public void dispose() {
        // TODO Auto-generated method stub
        
    }

    @Override
    protected void completeUserInput(T completion) {
        // TODO Auto-generated method stub
        
    }

}
  1. I tried to dispose previous autocompletion bindings everytime a new query was ran. But it didn't work, all bindings remained.
  2. I tried binding to an ObservableList where the ObservableList was fed by the Javafx Task query results, but the binding never would update to show the newly added values. It would bind to blank list and stay that way despite the fact the ObservableList would add the new values from the database.

I'm expecting to be able to type in a few characters, hit the database asynchronously so it doesn't freeze the UI. And then show valid results, while also eliminating any previous binding so the bindings don't stack on top of each other and cause confusion when the user autocompletes and it autocompletes to the wrong value because the application focus was on another binding popup, as can be seen in this image:

Shows a text field with two autocompletion bindings, one from the old input, and one from the current input. It should just show the top one.

Update: Adding a MCVE for others to troubleshoot and experiment with solutions:

Project Structure: MCVE Project

Code:

package com.autocomplete.example;

import org.controlsfx.control.textfield.AutoCompletionBinding;
import org.controlsfx.control.textfield.TextFields;

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

//Run project using mvn javafx:run
//You can see the bindings coninutally stack on top of eachother by using the ESC key on the keyboard to move the front one out of focus
public class AutocompleteExample extends Application {

private static final ObservableList<String> names1 = FXCollections.observableArrayList(
        "Alice", "Adam", "Alfred", "Amon", "Alfredo", "Al", "Albert"
);

private static final ObservableList<String> names2 = FXCollections.observableArrayList(
        "Bob", "Conner", "Robin", "Fred", "Freddy", "Edward", "Fredward", "Mariam"
);

@Override
public void start(Stage primaryStage) {
    TextField textField = new TextField();
    
    textField.setOnKeyTyped(event -> {
        AutoCompletionBinding<String> nameBinding = null;
        String input = textField.getText().toUpperCase();
        if (input.length() == 2){
            if (input.startsWith("A")) {
                if (nameBinding != null) nameBinding.dispose();
                nameBinding = TextFields.bindAutoCompletion(textField, names1);
                nameBinding.setOnAutoCompleted(val -> System.out.println("You selected "+ val.getCompletion() +" from list 1."));
            } else {
                if (nameBinding != null) nameBinding.dispose();
                nameBinding = TextFields.bindAutoCompletion(textField, names2);
                nameBinding.setOnAutoCompleted(val -> System.out.println("You selected "+ val.getCompletion() +" from list 2."));
            }
        } else if (nameBinding != null && input.length() < 2) nameBinding.dispose();
    });

    VBox root = new VBox(10, textField);
    Scene scene = new Scene(root, 300, 200);
    primaryStage.setScene(scene);
    primaryStage.setTitle("Autocomplete Example");
    primaryStage.show();
}

public static void main(String[] args) {
    launch(args);
}

}

POM:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.autocomplete.example</groupId>
    <artifactId>AutocompleteExample</artifactId>
    <version>1.0-SNAPSHOT</version>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.release>21</maven.compiler.release>
        <javafx.version>21.0.4</javafx.version>
        <exec.mainClass>com.autocomplete.example.AutocompleteExample</exec.mainClass>
    </properties>
        <dependencies>
        <dependency>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-controls</artifactId>
            <version>${javafx.version}</version>
        </dependency>
        <dependency>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-fxml</artifactId>
            <version>${javafx.version}</version>
        </dependency>
        <dependency>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-base</artifactId>
            <version>${javafx.version}</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.neo4j.driver/neo4j-java-driver -->
        <dependency>
            <groupId>org.neo4j.driver</groupId>
            <artifactId>neo4j-java-driver</artifactId>
            <version>5.18.0</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.controlsfx/controlsfx -->
        <dependency>
            <groupId>org.controlsfx</groupId>
            <artifactId>controlsfx</artifactId>
            <version>11.2.0</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <release>${maven.compiler.release}</release>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.openjfx</groupId>
                <artifactId>javafx-maven-plugin</artifactId>
                <version>0.0.8</version>
                <executions>
                    <execution>
                        <!-- Default configuration for running -->
                        <!-- Usage: mvn clean javafx:run -->
                        <id>default-cli</id>
                        <configuration>
                            <mainClass>${exec.mainClass}</mainClass>
                            <options>
                                <option>--add-exports</option>
                                <option>javafx.base/com.sun.javafx.event=org.controlsfx.controls</option>
                                <option>--add-modules=javafx.base</option>
                            </options>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

module-info file:

module com.autocomplete.example {
requires javafx.base;
requires javafx.fxml;
requires transitive javafx.controls;
requires transitive javafx.graphics;
requires org.controlsfx.controls;

opens com.autocomplete.example to javafx.fxml;
exports com.autocomplete.example;
}

MCVE Being Ran with Double AutoCompletion Bindings


Solution

  • Thanks to @SedJ601 for the inspiration that led me to take a new approach which solved all my problems! In order to use results from a database, but also change the values that are bound to the textfield based on what is input. You must bind the textfield using a suggestion provider that is bound to a list. Then based on the On Key Typed, whenever the user input is at the length needed, you query the database and populate the query results in the list. This way, you only bind the textfield once, but constantly manipulate that list itself. I previously attempted to do this without using the suggestion provider using the method:

    TextFields.bindAutoCompletion(TextField tf, Collection<E> c);
    

    USING THE ABOVE METHOD will not work with manipulating the list. You must use the suggestion provider method below in your initialize() method:

    List<String> autoCompleteModels = new ArrayList<>();
    @FXML private void initialize(){
        TextFields
            .bindAutoCompletion(modelQuickSearchTextField, input -> {
                if (input.getUserText().length() < 2) {
                    return Collections.emptyList();
                }
                return autoCompleteModels.stream().filter(s -> s.toLowerCase().contains(input.getUserText().toLowerCase())).collect(Collectors.toList());
            })
            .setOnAutoCompleted( e -> {
                String model = e.getCompletion().split("\s\\|\s")[0];
                openExistingInventory(SimpleCypher.getModelData(model));
                modelQuickSearchTextField.clear();
            });
    }
    

    And then use this method "On Key Typed" (I have it as a seperate method since I'm using JavaFX FXML Scenebuilder, but you can also do textField.onKeyTyped())

    //TextField On Key Typed
    @FXML TextField modelQuickSearchTextField;
    @FXML private void modelAutoComplete() {
        String input = modelQuickSearchTextField.getText().toUpperCase();
        if (input.length() == 2) {
            Task<List<String>> queryTask = new Task<>() {
                @Override
                protected List<String> call() throws Exception {
                    List<String> resultModels = new ArrayList<>();
                    try (Session session = DatabaseConnection.getSession()) {
                        Result result = session.run("""
                                MATCH (mo:InventoryModel)
                                WHERE mo.id CONTAINS $textFieldInput 
                                CALL{
                                    WITH mo
                                    OPTIONAL MATCH (mo)-[:EXISTS_AS]->(:InventoryItem)-[hcs:HAS_CURRENT_STATUS]->(:Status{id:'AVAILABLE'})
                                    RETURN sum(hcs.qty) AS available
                                }
                                RETURN mo.id + ' | ' + toString(available)
                                """,
                                Values.parameters("textFieldInput", input));
                        while (result.hasNext()) {
                            Record record = result.next();
                            resultModels.add(record.get(0).asString());
                        }
                    }
                    return resultModels;
                }
            };
    
            queryTask.setOnSucceeded(event -> autoCompleteModels = queryTask.getValue());
    
            // Start the task asynchronously
            Thread queryThread = new Thread(queryTask);
            queryThread.setDaemon(true); // Set as daemon thread to allow application exit
            queryThread.start();
        }
    }