-
-
Notifications
You must be signed in to change notification settings - Fork 144
Conversation
Looking good! I'd like to submit this to the community for review. Could you post a couple of representative code examples in this PR? |
Here's an example of dash app where you can add cities to the graph. If you push the reset button, it will ask to confirm before removing the user cities from the graph. import json
import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output, State
app = dash.Dash()
app.scripts.config.serve_locally = True
app.css.config.serve_locally = True
data = [
{
'x': [32],
'y': [48],
'name': 'Washington',
'mode': 'markers',
'marker': {'size': 12}
},
{
'x': [12],
'y': [9],
'name': 'New York',
'mode': 'markers',
'marker': {'size': 12}
}
]
container_style = {'display':'flex', 'flex-direction': 'column'}
app.layout = html.Div([
html.Div(id='controls', style=container_style, children=[
dcc.Input(id='city-name', placeholder='City name'),
dcc.Input(id='x', placeholder='x'),
dcc.Input(id='y', placeholder='y'),
html.Button(id='add-button', children='add'),
html.Button(id='reset-button', children='Reset', n_clicks=0),
]),
html.Div([
dcc.Confirm(id='confirm', message='Warning, you will lose all custom input.'),
dcc.Graph(id='graph'),
html.Div(id='values', style={'display': 'None'})
]),
], style={'display': 'flex', 'justify-items': 'stretch', 'align-items': 'stretch'})
@app.callback(Output('values', 'children'),
[Input('add-button', 'n_clicks'), Input('confirm', 'result')],
[State('city-name', 'value'), State('x', 'value'), State('y', 'value'), State('values', 'children')])
def on_add_city(n_clicks, confirm_result, city_name, x, y, values):
if confirm_result:
return ''
if n_clicks > 0:
graph_values = json.loads(values) if values else list(data)
graph_values.append({
'x': [int(x)],
'y': [int(y)],
'name': city_name,
'mode': 'markers',
'marker': {'size': 12}
})
return json.dumps(graph_values)
@app.callback(Output('confirm', 'init'), [Input('reset-button', 'n_clicks')])
def on_click_confirm(n_clicks):
if n_clicks > 0:
return {'ask': True, 'value': 'confirmed'}
return
@app.callback(Output('graph', 'figure'), [Input('values', 'children')])
def on_change_data(values):
if values:
v = json.loads(values)
return {'data': v}
return {'data': data}
if __name__ == '__main__':
app.run_server(debug=True) |
I personally find the nested property structure a little too complex:
As a user, I think I would expect an API something like this: html.Div([
Button(
'Click me',
id='my-button',
n_clicks=0
),
ConfirmDialog(
id='my-confirm-dialog',
message='''
Are you sure?
''',
cancel_text='Cancel',
submit_text='Submit',
displayed=False,
cancel_n_clicks=0,
cancel_n_clicks_timestamp=0,
submit_n_clicks=0, # or just n_clicks
submit_n_clicks_timestamp=0,
),
dcc.Input(id='my-input'),
html.Div(id='my-output')
])
@app.callback(Output('my-confirm-dialog', 'displayed'), [Input('my-button', 'n_clicks')])
def display_confirm(n_clicks):
if n_clicks > 0:
return True
return False
@app.callback(Output('my-output', 'children'),
[Input('my-confirm-dialog', 'n_clicks_submit')],
[State('my-input', 'value')])
def display_confirm(n_clicks, value):
if n_clicks == 0:
raise dash.exceptions.PreventUpdate()
# do something dangerous
return 'your results are: ...' A few notes about this API:
Now, in many cases, this pattern is going always be tied with a button and is always going to have this tedious callback structure that displays the modal when the user clicks the button: @app.callback(Output('my-confirm-dialog', 'displayed'), [Input('my-button', 'n_clicks')])
def display_confirm(n_clicks):
if n_clicks > 0:
return True
return false Given that this is going to be really common, I wonder if we should have an additional component that takes a Dash component as html.Div([
ConfirmDialogProvider(
children=Button('Click me'),
id='my-confirm-dialog',
message='''
Are you sure?
''',
cancel_text='Cancel',
submit_text='Submit',
displayed=False,
cancel_n_clicks=0,
cancel_n_clicks_timestamp=0,
submit_n_clicks=0, # or just n_clicks
submit_n_clicks_timestamp=0,
),
dcc.Input(id='my-input'),
html.Div(id='my-output')
])
@app.callback(Output('my-output', 'children'),
[Input('my-confirm-dialog', 'n_clicks_submit')],
[State('my-input', 'value')])
def display_confirm(n_clicks, value):
if n_clicks == 0:
raise dash.exceptions.PreventUpdate()
# do something dangerous
return 'your results are: ...' cc @plotly/dash for your review as well |
I'm concerned about these |
bf68532
to
14bc6e1
Compare
14bc6e1
to
86d86e2
Compare
These checks against are |
test/test_integration.py
Outdated
button.click() | ||
time.sleep(1) | ||
|
||
self.driver.switch_to.alert.accept() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎉 very nice!
/** | ||
* Number of times the modal was submited or canceled. | ||
*/ | ||
n_clicks: PropTypes.number, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given that we have submit_n_clicks
and cancel_n_clicks
, is there a use case for the general n_clicks
?
/** | ||
* Set to true to send the popup. | ||
*/ | ||
send_confirm: PropTypes.bool, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For some reason, "send"
doesn't feel right to me. What about display_confirm
? Or maybe just displayed
? Or display_confirm_dialog
? Any other thoughts @plotly/dash ?
/** | ||
* Is the modal currently displayed. | ||
*/ | ||
displayed: PropTypes.bool, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that I thought that displayed
would take the place of send_confirm
:
@app.callback(Output('confirm', 'displayed'), [Input('my-button', 'n_clicks'])
def display_modal(n_clicks):
return boolean(n_clicks)
Does that work? Or am I missing a functional difference between displayed
and send_confirm
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Send_confirm is for activating the modal, it get sets to false after fire, displayed was for telling if the confirmation was currently showing. I just changed for just displayed and it works the same.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think a good rule to keep in mind here and with most functional/React style coding is that prop names should not be verbs, especially not imperative ones like send
but rather descriptions of state like displayed
:)
import {Component} from 'react'; | ||
|
||
/** | ||
* ConfirmDialog wraps window.confirm |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the message that will appear in the component's help(dcc.ConfirmDialog)
, so let's expand this a bit. The Dash users probably don't know what window.confirm
is referring to. Perhaps something like:
ConfirmDialog is used to display the browser's native "confirm" modal, with an optional message and two buttons ("OK" and "Cancel"). This
ConfirmDialog
can be used in conjunction with buttons when the user is performing an action that should require an extra step of verification.
And then similarly for the ConfirmDialogProvider
. For the ConfirmDialogProvider
, we should mention that you can pass in a button directly as children
const { id, setProps, children } = this.props; | ||
|
||
// Will lose the previous onClick of the child | ||
const wrapClick = (child) => React.cloneElement(child, {onClick: () => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice, this seems like a good solution.
test/test_integration.py
Outdated
time.sleep(0.5) | ||
self.wait_for_text_to_equal('#confirmed', 'canceled') | ||
|
||
self.snapshot('confirmation -> canceled') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very nice tests! I'd like to see a couple of other things:
- A similar test for the
dcc.ConfirmDialogProvider
- Let's test that the callback is called an appropriate number of times. In other tests, I use a thread/multiprocess-safe variables to do that, see e.g. the "
call_count
" variables here: https://github.com/plotly/dash-renderer/blob/master/tests/test_render.py#L478-L484
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added a test for the provider and put a call count variable. The call_count get increased normally in my local tests but in circleci it get increased by 6 instead of two, I put the value to be the n_clicks instead for the tests to work on circleci but I think there's an error.
Looking good @T4rk1n ! I just made a few other comments. Let's also make a prerelease of this package so that some community members can easily take it for a spin. I wrote some instructions on how to publish prereleases in this comment here: #215 (comment) |
21c9079
to
ab17240
Compare
ab17240
to
d4adc7a
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall this looks good! Just a few comments around the docstrings, adding timestamp
to submit
and cancel
. I'm 👍 with this once those changes are made
💃
/** | ||
* Wrap children onClick to send a confirmation dialog. | ||
* You can add a button directly as a children: | ||
* `dcc.ConfirmDialogProvider(html.Button('click me', id='btn'), id='confirm')` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Our users probably won't know what onClick
means, so let's change this to:
A wrapper component that will display a confirmation dialog when its child component has been clicked on.
For example:
dcc.ConfirmDialogProvider(
children=html.Button('Click Me'),
message='Danger - Are you sure you want to continue?',
id='confirm'
)
/** | ||
* Number of times the popup was canceled. | ||
*/ | ||
cancel_n_clicks: PropTypes.number, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we add submit_n_clicks_timestamp
and cancel_n_clicks_timestamp
here and in the ConfirmDialog
component?
/** | ||
* Number of times the modal was submited or canceled. | ||
*/ | ||
n_clicks: PropTypes.number, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm still wondering if there is a use case for this property given that we have submit_n_clicks
and cancel_n_clicks
. Thoughts @plotly/dash ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I add the timestamps for submit and cancel, I don't see one as I was only using it to know which one was clicked in the test and it wasn't a real use case. I will remove it.
*/ | ||
n_clicks_timestamp: PropTypes.number, | ||
/** | ||
* Number of times the submit was clicked |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Number of times the submit button was clicked
@T4rk1n you have the 💃 to merge this ✔️ We have interested parties waiting for release. Thanks! |
6592fff
to
ac90a45
Compare
# Conflicts: # CHANGELOG.md # test/test_integration.py
Simple example:
Another example with callback controlled confirm dialog: