Js event management in clientside callback

Hello, i am new to dash. i am trying to have keyboard key down for some specific keys in js. In the event it should access dcc.store and change the values.
in dash:

app.clientside_callback(
    ClientsideFunction(namespace='clientside', function_name='update_selection_box'),
    output=Output('selection-coords', 'data',allow_duplicate=True),
    inputs=[Input('selection-coords', 'data')],
    prevent_initial_call=True,
)

and in custom_script.js:

document.addEventListener('DOMContentLoaded', function() {
    window.dash_clientside = Object.assign({}, window.dash_clientside, {
        clientside: {
            update_selection_box: function(coords) {
                document.addEventListener('keydown', function(event) {
                    if (!coords || Object.keys(coords).length === 0) return;
                    newCoords=parseIfJSON(coords)
                    //let newCoords = Object.assign({}, coords);
                    const moveAmount = 10; // Adjust based on your graph's scale
                    console.log("original " +newCoords.x0)
                    if (event.key === 'ArrowLeft') {
                        newCoords.x0 -= moveAmount;
                        newCoords.x1 -= moveAmount;
                        newCoords.finalize=false
                        console.log("decreased " +newCoords.x0)
                    } else if (event.key === 'ArrowRight') {
                        newCoords.x0 += moveAmount;
                        newCoords.x1 += moveAmount;
                        newCoords.finalize=false
                        console.log("increased " +newCoords.x0)

                    }
                    else if (event.key === 'Enter'){
                        newCoords.finalize=true
                    }
                    dash_clientside.set_props("selection-coords", {data:newCoords})
                });
                return window.dash_clientside.no_update;
            }
        }
    });
});

problem is, each time i press a arrow button, the event triggers one+the numer of times i already pressed. so in trird press the event will trigger 4 times. the reason is probably each time theupdate_selection_box function is called, it attaches a new keydown event listener without removing the previous ones. But I dont know how to solve this ?

Hello @mainul,

This is because you are using the data as a callback, the callback only exists to add an event listener to the DOM element, you are adding an event listener to the element each time you are updating the data.

The callback should look like this:


app.clientside_callback(
    ClientsideFunction(namespace='clientside', function_name='update_selection_box'),
    output=Output('selection-coords', 'id’,allow_duplicate=True),
    inputs=[Input('selection-coords', ‘Id’)],
    prevent_initial_call=True,
)

This will then add a listener to the whole document and update the data accordingly.

Please ignore the typing mistakes, it’s autocorrecting on my phone…

I used data as a callback because I needed to use the data in the event function.

Yes. That’s fine, but you only need to add the event listener once.

The data callback can be used to then take that data else where.

In fact you don’t even need a callback to apply this.

Put this js file in your assets folder and it should work.

document.addEventListener('keydown', function(event) {
                    if (!coords || Object.keys(coords).length === 0) return;
                    newCoords=parseIfJSON(coords)
                    //let newCoords = Object.assign({}, coords);
                    const moveAmount = 10; // Adjust based on your graph's scale
                    console.log("original " +newCoords.x0)
                    if (event.key === 'ArrowLeft') {
                        newCoords.x0 -= moveAmount;
                        newCoords.x1 -= moveAmount;
                        newCoords.finalize=false
                        console.log("decreased " +newCoords.x0)
                    } else if (event.key === 'ArrowRight') {
                        newCoords.x0 += moveAmount;
                        newCoords.x1 += moveAmount;
                        newCoords.finalize=false
                        console.log("increased " +newCoords.x0)

                    }
                    else if (event.key === 'Enter'){
                        newCoords.finalize=true
                    }
                    dash_clientside.set_props("selection-coords", {data:newCoords})
                })

If you dcc.Store is a local or session store, you can access it on the Js side via localStorage.getItem and sessionStorage.getItem.

If the store is in memory, it becomes harder.

If it’s local, do this:

var coords = JSON.parse(localStorage.get(“selection-coords”) || [])
if (!coords || Object.keys(coords).length === 0) return;

Once the coords are set, will this function always be driving it? Or can it start from different places?

thanks for the reply. the localStorage and sessionStorage both are working after removing the clientside callback. One problem I still have is , after some keyboard operation new coords are saved in the local/session storage and then if I reload the page I get the following error message in the browser:

In the callback for output(s):
  selection-coords.data@46d0580ef4cf90229cdf19676f2b3627
Output 0 (selection-coords.data@46d0580ef4cf90229cdf19676f2b3627) is already in use.
To resolve this, set `allow_duplicate=True` on
duplicate outputs, or combine the outputs into
one callback function, distinguishing the trigger
by using `dash.callback_context` if necessary.

but in the dash code i already made
Output('selection-coords', 'data', allow_duplicate=True)

in every output.

Ah, this is more than likely due to having the same inputs for both of the callbacks.

In dash, there is a serialization of the ids for the output to make sure that these can be called uniquely.

My way around this is to add an unused input, like the id of the component. Since if you are using prevent_initial_call=True this id doesnt trigger it.

Using the old code, it would look like this:

app.clientside_callback(
    ClientsideFunction(namespace='clientside', function_name='update_selection_box'),
    output=Output('selection-coords', 'data',allow_duplicate=True),
    inputs=[Input('selection-coords', 'data'), Input('selection-coords', 'id')],
    prevent_initial_call=True,
)

The secondary id makes it so that this would have a unique serial vs this:

app.clientside_callback(
    ClientsideFunction(namespace='clientside', function_name='update_selection_box'),
    output=Output('selection-coords', 'data',allow_duplicate=True),
    inputs=[Input('selection-coords', 'data')],
    prevent_initial_call=True,
)

Even though we would know that these are exactly the same. :smiley: