How to get trigger from keyboard?

Dear all,

In my dash app, I have succeeded to have undo/redo functions from callbacks with a button component. Now, I want to upgrade it with shortcut using keyboard press (such as Ctrl+Z and Ctrl+Y). So, I wonder if there is any way to get triggers when I press keyboard keys?

Screenshot 2023-07-17 234008

Cheers,

1 Like

Hello @addsc6,

You can make your own event listeners via JS that listen to the keydown event and test that event.ctrlKey && event.key == 'Z' and event.ctrlKey && event.key == 'X' respectively. And place these functions in a JS file.

Or, you could use dash-extensions event listener:

3 Likes

Many thanks!
I have obtained the pressed key data from JS and seen it in the browser. However, I’m not be able to print out data in console.
I prepared a script below, what actually went wrong with it?
It seems like the data from keyboard bypassing python server.

import dash
from dash import Input, Output, State, ClientsideFunction, html

app = dash.Dash(__name__)

app.layout = html.Div([
    html.Button("Button", id="button"),
    html.Div(id="output"),
    html.Div(id="output2", children="This is output2")
])

app.clientside_callback(
    """
        function(n_clicks) {
            window.addEventListener("keydown", function(event) {
                document.getElementById("output").textContent = event.key;
                return "key has pressed!!"; ///event.key;
            });
            return null; // Return null to prevent any updates on the output        
        }
    """,
    Output("output", "children"),
    [Input("button", "n_clicks")]
)

@app.callback(
    Output("output2", "children"),
    Input("output", "children"),
)
def show_value(value):
    print(value)

    return dash.no_update


if __name__ == "__main__":
    app.run_server(debug=True)

JS event listeners cannot directly adjust react/dash props.

You can have the event listener trigger a click event on something that is set up to do what you want to.

1 Like

This kind of trigger is exactly what I wanted.
Could you pls explain a bit more detail?

Yes. Now, I’m seeking for the way to trigger a dash callback function upon an event of keyboard press.
Any idea how to do so?

Sure thing, here you are:

import dash
from dash import Input, Output, State, ClientsideFunction, html, dcc, ctx

app = dash.Dash(__name__)

app.layout = html.Div([
    html.Button("Undo-Button", id="undoButton"),
    html.Button("Redo-Button", id="redoButton"),
    html.Div(id='output')
])

app.clientside_callback(
    """
        function(id) {
            document.addEventListener("keydown", function(event) {
                if (event.ctrlKey) {
                    if (event.key == 'z') {
                        document.getElementById('undoButton').click()
                        event.stopPropogation()
                    }
                    if (event.key == 'x') {
                        document.getElementById('redoButton').click()
                        event.stopPropogation()
                    }
                }
            });
            return window.dash_clientside.no_update       
        }
    """,
    Output("undoButton", "id"),
    Input("undoButton", "id")
)

@app.callback(
    Output("output", "children"),
    Input("undoButton", "n_clicks"),
    Input("redoButton", "n_clicks"),
    prevent_initial_call=True
)
def show_value(n1, n2):
    return ctx.triggered_id


if __name__ == "__main__":
    app.run_server(debug=True)
4 Likes

Excellent!!! This works perfectly well.
Many many thanks!!

1 Like

Wow this is great. Didn’t know this was even possible!

2 Likes

Try this component, pretty easy, no need to write any javascript:

https://fuc.feffery.tech/FefferyKeyPress

1 Like

Hello @CNFeffery,

There is this same thing available from dash-extensions, the cool thing about using Js is that you can associate the commands with specific elements on the page rather than having the whole document listen.

That’s right, and it’s also the reason I created so many components in feffery-utils-components ( https://fuc.feffery.tech/what-is-fuc ) to prevent writing unnecessary javascript code.:blush:

@jinnyzor This look great. :slight_smile:
Does this work with pattern matching callback? Thanks!!!

What exactly do you mean?

Thanks @jinnyzor , For your example above, it works perfect. However I am wondering, does it work for Pattern-Matching Callbacks | Dash for Python Documentation | Plotly? Thank you!!

Does what work?

The event listener in the above example is in added to the whole document, to add it to a pattern-matched element, you’d need to do something like this:

assets/js file

function stringifyId(id) {
  if (typeof id !== "object") {
    return id;
  }
  const stringifyVal = (v) => (v && v.wild) || JSON.stringify(v);
  const parts = Object.keys(id)
    .sort()
    .map((k) => JSON.stringify(k) + ":" + stringifyVal(id[k]));
  return "{" + parts.join(",") + "}";
}

app.py

import dash
from dash import Input, Output, State, ClientsideFunction, html, dcc, ctx, MATCH
import json

app = dash.Dash(__name__)

app.layout = html.Div([
    html.Div([html.Button("Undo-Button", id={'index': x, 'type':"undoButton"}),
    html.Button("Redo-Button", id={'index': x, 'type':"redoButton"}),
    dcc.Input(id={'index': x, 'type':'input'}),
    html.Div(id={'index': x, 'type':'output'}, style={'backgroundColor': 'green', 'width': '300px', 'height': '100px', 'color': 'white'})]) for x in range(3)
])

app.clientside_callback(
    """
        function(inp, undo, redo) {
            document.getElementById(stringifyId(inp)).addEventListener("keydown", function(event) {
                if (event.ctrlKey) {
                    if (event.key == 'z') {
                        document.getElementById(stringifyId(undo)).click()
                        event.stopPropagation()
                    }
                    if (event.key == 'x') {
                        document.getElementById(stringifyId(redo)).click()
                        event.stopPropagation()
                    }
                }
            });
            return window.dash_clientside.no_update       
        }
    """,
    Output({'index': MATCH, 'type':"undoButton"}, "id"),
    Input({'index': MATCH, 'type':"input"}, "id"),
    State({'index': MATCH, 'type':"undoButton"}, "id"),
    State({'index': MATCH, 'type':"redoButton"}, "id"),
)

@app.callback(
    Output({'index': MATCH, 'type': "output"}, "children"),
    Input({'index': MATCH, 'type': "undoButton"}, "n_clicks"),
    Input({'index': MATCH, 'type': "redoButton"}, "n_clicks"),
    prevent_initial_call=True
)
def show_value(n1, n2):
    return json.dumps(ctx.triggered_id)


if __name__ == "__main__":
    app.run_server(debug=True)

Give this a test and see how you like it, the event listener here is added to the inputs.


Here it is added to the div, note that you will need something to actually be receiving the keydown, which then bubbles up to the div:

import dash
from dash import Input, Output, State, ClientsideFunction, html, dcc, ctx, MATCH, Patch
import json

app = dash.Dash(__name__)

app.layout = html.Div([
    html.Div([html.Button("Undo-Button", id={'index': x, 'type':"undoButton"}),
    html.Button("Redo-Button", id={'index': x, 'type':"redoButton"}),
    html.Div(id={'index': x, 'type':'output'}, children=[dcc.Input(id={'index': x, 'type':'input'})], style={'backgroundColor': 'green', 'width': '300px', 'height': '100px', 'color': 'white'})]) for x in range(3)
])

app.clientside_callback(
    """
        function(out, undo, redo) {
            document.getElementById(stringifyId(out)).addEventListener("keydown", function(event) {
                if (event.ctrlKey) {
                    if (event.key == 'z') {
                        document.getElementById(stringifyId(undo)).click()
                        event.stopPropagation()
                    }
                    if (event.key == 'x') {
                        document.getElementById(stringifyId(redo)).click()
                        event.stopPropagation()
                    }
                }
            });
            return window.dash_clientside.no_update       
        }
    """,
    Output({'index': MATCH, 'type':"undoButton"}, "id"),
    Input({'index': MATCH, 'type':"output"}, "id"),
    State({'index': MATCH, 'type':"undoButton"}, "id"),
    State({'index': MATCH, 'type':"redoButton"}, "id"),
)

@app.callback(
    Output({'index': MATCH, 'type': "output"}, "children"),
    Input({'index': MATCH, 'type': "undoButton"}, "n_clicks"),
    Input({'index': MATCH, 'type': "redoButton"}, "n_clicks"),
    prevent_initial_call=True
)
def show_value(n1, n2):
    child = Patch()
    child[1] = json.dumps(ctx.triggered_id)
    return child


if __name__ == "__main__":
    app.run_server(debug=True)
2 Likes

Hey @jinnyzor ,

I am trying to do something very similar (I assign certain keys to open modals (from dash_bootstrap_components)). However, my page has a few Input components (from dcc) and I noticed that when typing in those, the callbacks also get triggered. Is it possible to prevent this? I essentially only want to trigger the JavaScript callback while the user is not typing in an Input field (or alternatively while a specific element is in focus).

If there is no straightforward solution, I suppose a workaround would be to include having to press the Ctrl key as in the original example but that is not the most convenient solution for my purposes.

Thank you in advance!

Hello @RobinWeiler,

You can always check to make sure that the event.target nodeName isnt an INPUT.

1 Like

Thank you for the fast response!

Would this be via if event.target.nodeName != 'INPUT' ?
When trying this, I get an error: undefined is not an object (evaluating '(_dc$namespace = dc[namespace])[function_name]')
I am not very familiar with JavaScript so apologies if I am misunderstanding something.

See the code below derived from your code example:

import dash
from dash import Input, Output, html, dcc, ctx

app = dash.Dash(__name__)

app.layout = html.Div([
    html.Button("Undo-Button", id="undoButton"),
    html.Button("Redo-Button", id="redoButton"),
    dcc.Input(id="test-input", value='placeholder'),
    html.Div(id='output')
])

app.clientside_callback(
    """
        function(id) {
            document.addEventListener("keydown", function(event) {
                if event.target.nodeName != 'INPUT' {
                    if (event.key == 'ArrowLeft') {
                        document.getElementById('undoButton').click()
                        event.stopPropogation()
                    }
                    if (event.key == 'ArrowRight') {
                        document.getElementById('redoButton').click()
                        event.stopPropogation()
                    }
                }
            });
            return window.dash_clientside.no_update       
        }
    """,
    Output("undoButton", "id"),
    Input("undoButton", "id")
)

@app.callback(
    Output("output", "children"),
    Input("undoButton", "n_clicks"),
    Input("redoButton", "n_clicks"),
    prevent_initial_call=True
)
def show_value(n1, n2):
    return ctx.triggered_id


if __name__ == "__main__":
    app.run_server(debug=True)

No problem, @RobinWeiler,

Js if statements need to be wrapped in (). :grin:

1 Like