pythonplotly-dash

Dash Input not callingback after Google Places Autocompletion


I have the following callback

@app.callback(Output("full_address", "data"),
        Input("corrected_address", "value"))
        def store_address(inputdata):
            print(inputdata)
            return inputdata

which is tied to the element created by

dbc.Input(id="corrected_address")

This element uses a Google Places auto complete element:

var input = document.getElementById('corrected_address');
var autocomplete = new google.maps.places.Autocomplete(input);
autocomplete.setFields(['address_component', 'geometry', 'name']);

The auto complete shows up as expected. enter image description here

However, when completed, the callback is not triggered enter image description here

Updating the field by adding or removing a character updates the value as expected. enter image description here

I feel like I'm missing something simple here. How can I get the callback to trigger?

I'm using dash and dash-bootstrap-components, Python 3.10.12.


Solution

  • I've found a solution that works quite nicely in my opinion. Due to Dash ignoring changes to values from JavaScript, going as far as to ignore manually issued change events, dash_extensions EventListener can be used alongside a custom event and a serverside callback to update the fields value appropriately on the backend.

    from dash import Dash, html, dcc, clientside_callback
    from dash_extensions import EventListener
    from dash.exceptions import PreventUpdate
    from dash.dependencies import Input, Output, State, ALL
    import dash_bootstrap_components as dbc
    
    
    GAPI_STRING = ""
    
    external_scripts = [
        'https://maps.googleapis.com/maps/api/js?key='+GAPI_STRING+'&libraries=places',
    ]
    app = Dash(
        __name__,
        suppress_callback_exceptions=True,
        external_stylesheets=[dbc.themes.BOOTSTRAP],
        external_scripts=external_scripts,
    )
    
    
    # The event used to pass the value of the autocomplete field from the client to the server
    manual_notify = {"event": "autocomplete_finished", "props": ["srcElement.value"]}
    
    app.layout = [
        html.Div([html.Label("Current Address:", id="address_label")], id="display_div"),
        html.Div([
            EventListener(
                [
                    dbc.Input(id="auto_complete_field")
                ], events=[manual_notify], logging=True, id="event_listener"
            ),
        ], id="auto_complete_div")
    ]
    
    clientside_callback(
        """
        // Used to initiate autocomplete after the Dash form is created
        // This function requires the element to be present,
        // and if it were in a normal external script, it would not run
        // and auto complete would not initialize.
        
        // This callback essentially takes in the element when it's created
        // Runs the autocomplete setup functions, and returns the element as
        // it is.
        function(facade1) {
            // setup autcomplete
            var input = document.getElementById('auto_complete_field');
            var autocomplete = new google.maps.places.Autocomplete(input);
            autocomplete.setFields(['address_component', 'geometry', 'name']);
            
            // dispatch the "autocomplete_finished" event when "place_changed" is dispatched.
            autocomplete.addListener('place_changed', () => input.dispatchEvent(new Event("autocomplete_finished")));
            return facade1;
        }
        """,
        Output("auto_complete_div", "children"),
        Input("auto_complete_div", "children")
    )
    
    # When the autocomplete_finished event is dispatched
    # this callback will handle it, and outputs the full
    # address to the autocomplete fields value property,
    # syncing the backend value with the front end value
    @app.callback(
        Output("auto_complete_field", "value"),
        Input("event_listener", "n_events"),
        State("event_listener", "event"))
    def store_address(n_events, event):
        if event is None:
            raise PreventUpdate()
        return event["srcElement.value"]
    
    # This updates the current address label using the
    # autocomplete fields value property as the input,
    # proving that the internal value is the same as
    # the one in the clients input field.
    @app.callback(Output("address_label", "children"), Input("auto_complete_field", "value"))
    def print_address(field_value):
        if field_value == None:
            raise PreventUpdate()
        return "Current Address: " + field_value
    
    if __name__ == "__main__":
        app.run(debug=True)
    

    I hope this helps somebody! This was a doozy to figure out.