跳到主要内容
版本:0.5.0

利用dash-json-grid构建一个JSON编辑器

本文解释了以下脚本的设计思路:

editor.py

该范例可以独立运行。运行之前,需要安装额外的依赖包:

pip install dash-json-grid[example]

以上命令用来安装下列包:

  • Dash Bootstrap Components: 用来在本范例中、提供全局样式和模态(对话框)窗口。
  • Dash Ace: 用来在本范例中、提供代码(JSON数据)编辑器。

安装完成后,可以下载本范例脚本,并直接运行即可。 Then this script can be downloaded and run indepdently.

一个利用dash-json-grid实现 JSON 编辑器的范例
demo-editor

该范例展示了以下特性:

  • 一个透过使用dash-json-grid实现的、用来修改JSON数据的应用。
  • 每当页面刷新时,数据重置为初始状态。
  • 选中所展示数据的任何一部分,将唤起一个对话框。
  • 在对话框中,用户可以修改或删除选中的数据,并决定是否要确认这些修改。要删除数据,用户只需要令编辑器留空即可。

定义布局

在该范例中,布局主要包含两部分:

dcc.Loading(
className="mb-2",
delay_show=500,
children=djg.DashJsonGrid(
id="viewer",
data=test_data,
highlight_selected=True,
theme="defaultLight",
),
)

其中,定义了以下组件:

  • djg.DashJsonGrid: 本项目提供的JSON显示工具。
  • dbc.Modal: 只在用户选中JSON显示器的一部分时弹出的对话框。
  • da.DashAceEditor: 用来修改JSON数据的代码编辑器。
  • dbc.Alert: 只在修改JSON数据出错时,弹出的警告条。

定义callback

该范例中定义了三个callback:

@app.callback(
Output("editor", "is_open"),
# 需要以下Output来确保每次点击都能唤起对话框。
Output("viewer", "selected_path"),
Input("viewer", "selected_path"),
# 由于不希望在修改失败的时候关闭对话框,于是使用"editor-alert"取代"editor-confirm-btn"。
Input("editor-alert", "children"),
Input("editor-cancel-btn", "n_clicks"),
prevent_initial_call=True,
)
def toggle_editor(
route: djg.mixins.Route, altert_text: Optional[str], n_clicks_cancel: int
):
trigger_id = dash.ctx.triggered_id
if trigger_id == "viewer":
if route and not djg.DashJsonGrid.compare_routes(route, []):
return True, dash.no_update
elif trigger_id == "editor-alert":
if not altert_text:
return False, []
elif trigger_id == "editor-cancel-btn":
if n_clicks_cancel:
return False, []

return dash.no_update, dash.no_update

第一个callback用来控制对话框的显示与否。以下三个callback输入可以触发该callback:

  • viewer: 当显示器的一部分被选中时,打开对话框。
  • editor-alert: 当警告条的文字被重设、而又没有提供任何警告文字时,这表示修改顺利完成。由此关闭对话框。若警告文字存在,则不关闭对话框。
  • editor-cancel-btn: 当点击Cancel(取消)按钮时,关闭对话框。

注意该callback还有第二个输出。该输出用来在每次对话框关闭时,确保selected_path的值重置。默认情况下,selected_path只会在它的值改变时触发callback。因此,将该值重置、可以确保即使用户反复点击显示器的同一部分,也能触发callback。


第二个callback决定了修改数据时的行为。

@app.callback(
Output("viewer", "data"),
Output("editor-alert", "is_open"),
Output("editor-alert", "children"),
Input("editor-confirm-btn", "n_clicks"),
State("editor-text", "value"),
State("viewer", "selected_path"),
State("viewer", "data"),
prevent_initial_call=True,
)
def confirm_data_change(
n_clicks_confirm: int, modified_data: str, route: djg.mixins.Route, data: Any
):
if not n_clicks_confirm:
return dash.no_update, dash.no_update, dash.no_update

if not route:
return dash.no_update, dash.no_update, dash.no_update

if not modified_data:
# 这一try-except块略过了试图删除"undefined"值时触发的错误。
try:
djg.DashJsonGrid.delete_data_by_route(data, route)
except (KeyError, IndexError):
pass
# 当整个数据都被删除时,显示器将无法选中。因此,不允许这种情况出现。
if not data:
return dash.no_update, True, "Error: It is not allowed to delete all data."
return data, "", False

# 这一try-except块用来在用户提供了无效JSON数据时,向用户提示错误。
try:
decoded_modified_data = json.loads(modified_data)
except json.JSONDecodeError as exc:
logging.error(exc, stack_info=True)
return (
dash.no_update,
True,
"{0}: {1}.".format(exc.__class__.__name__, str(exc)),
)

if "selected_data" not in decoded_modified_data:
# 这一try-except块略过了试图删除"undefined"值时触发的错误。
try:
djg.DashJsonGrid.delete_data_by_route(data, route)
except (KeyError, IndexError):
pass
# 当整个数据都被删除时,显示器将无法选中。因此,不允许这种情况出现。
if not data:
return dash.no_update, True, "Error: It is not allowed to delete all data."
return data, "", False

# 在用户所修改的数据不匹配原数据格式时,向用户提示错误。例如,当一个表格的列被选中时,
# 若用户提供的数据包含了和原数据不匹配的行数,则此处将会触发一个错误。
try:
djg.DashJsonGrid.update_data_by_route(
data, route, decoded_modified_data["selected_data"]
)
except (KeyError, IndexError, TypeError, ValueError) as exc:
logging.error(exc, stack_info=True)
return (
dash.no_update,
True,
"{0}: {1}.".format(exc.__class__.__name__, str(exc)),
)

# 当整个数据都被删除时,显示器将无法选中。因此,不允许这种情况出现。
if not data:
return dash.no_update, True, "Error: It is not allowed to delete all data."

return data, "", False

这一callback只在点击Confirm(确认)按钮、且对话框打开时触发。它的输出指向了两个组件:若修改成功,则使用修改后的数据、取代viewer的原数据。若修改失败,则将callback抛出的错误信息,展示到警告条组件editor-alert上。

修改数据的过程已然考虑到了各种特殊情形。这一过程可以总结如下:

  • 当对话框打开时,数据会格式化为如下形式的JSON字典:
    {"selected_data": ...}
    其中...是所选中的数据。
  • 若文字框留空、或原数据中的关键字selected_data被移除,则尝试删除选中的数据。
  • 若删除数据、会导致整个数据变为空,则会展示错误信息、并取消修改。这是因为将viewer的数据设置为空、会导致其再也无法选中。
  • 考虑了各种错误情形。例如,会验证用户提供的JSON输入。且输入数据的格式应当和原数据相匹配。

最后一个callback定义了选中显示器的一部分、且对话框唤起时的行为:

@app.callback(
Output("editor-text", "value"),
Input("viewer", "selected_path"),
State("viewer", "data"),
prevent_initial_call=True,
)
def configure_current_editor_data(route: djg.mixins.Route, data: Any):
if not route:
return dash.no_update

# 这一try-except块用来捕获选中了"undefined"的特殊情况。若一个单元标记为"undefined",
# 这表明指向它的路径,实际没有指向任何值,因此此处返回的原始数据也应当为空。
try:
sel_data = djg.DashJsonGrid.get_data_by_route(data, route)
except (KeyError, IndexError): # Select an undefined cell.
return ""

selected_data = {"selected_data": sel_data}

return json.dumps(selected_data, ensure_ascii=False, indent=2)

当用户选中viewer的一部分时,selected_path属性就会触发这一callback。该属性提供的路径、用来定位所点击的值,并将值转化为JSON字符串,最后令编辑器的内容格式化为以下形式:

{"selected_data": ...}

其中...是选中的数据。