Skip to content

Commit 308cee3

Browse files
authored
Merge pull request #39 from plotly/callbacks_refactoring
callback! function refactoring
2 parents b302856 + 7c36096 commit 308cee3

31 files changed

+394
-223
lines changed

README.md

+9-10
Original file line numberDiff line numberDiff line change
@@ -81,17 +81,15 @@ julia> app.layout = html_div() do
8181
html_div(id = "my-div")
8282
end
8383
84-
julia> callback!(app, callid"my-id.value => my-div.children") do input_value
84+
julia> callback!(app, Output("my-div", "children"), Input("my-id", "value")) do input_value
8585
"You've entered $(input_value)"
8686
end
8787
8888
julia> run_server(app, "0.0.0.0", 8080)
8989
```
90-
91-
* You can make your dashboard interactive by register callbacks for changes in frontend with function ``callback!(func::Function, app::Dash, id::CallbackId)``
92-
* Inputs and outputs (and states, see below) of callback are described by struct `CallbackId` which can easily created by string macro `callid""`
93-
* `callid""` parse string in form ``"[{state1 [,...]}] input1[,...] => output1[,...]"`` where all items is ``"<element id>.<property name>"``
94-
* Callback functions must have the signature(states..., inputs...), and provide a return value comparable (in terms of number of elements) to the outputs being updated.
90+
* You can make your dashboard interactive by register callbacks for changes in frontend with function ``callback!(func::Function, app::Dash, output, input, state)``
91+
* Inputs and outputs (and states, see below) of callback can be `Input`, `Output`, `State` objects or vectors of this objects
92+
* Callback function must have the signature(inputs..., states...), and provide a return value comparable (in terms of number of elements) to the outputs being updated.
9593

9694
### States and Multiple Outputs
9795
```jldoctest
@@ -107,7 +105,7 @@ julia> app.layout = html_div() do
107105
html_div(id = "my-div2")
108106
end
109107
110-
julia> callback!(app, callid"{my-id.type} my-id.value => my-div.children, my-div2.children") do state_value, input_value
108+
julia> callback!(app, [Output("my-div"."children"), Output("my-div2"."children")], Input("my-id", "value"), State("my-id", "type")) do input_value, state_value
111109
"You've entered $(input_value) in input with type $(state_value)",
112110
"You've entered $(input_value)"
113111
end
@@ -163,9 +161,10 @@ def update_output(n_clicks, state1, state2):
163161
```
164162
* Dash.jl:
165163
```julia
166-
callback!(app, callid"""{state1.value, state2.value}
167-
submit-button.n_clicks
168-
=> output.children""" ) do state1, state2, n_clicks
164+
callback!(app, Output("output", "children"),
165+
[Input("submit-button", "n_clicks")],
166+
[State("state-1", "value"),
167+
State("state-2", "value")]) do n_clicks, state1, state2
169168
.....
170169
end
171170
```

src/Dash.jl

+3-2
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ include("Front.jl")
88
import .Front
99
using .Components
1010

11-
export dash, Component, Front, <|, @callid_str, CallbackId, callback!,
11+
export dash, Component, Front, callback!,
1212
enable_dev_tools!, ClientsideFunction,
13-
run_server, PreventUpdate, no_update, @var_str
13+
run_server, PreventUpdate, no_update, @var_str,
14+
Input, Output, State, make_handler
1415

1516

1617

src/app/callbacks.jl

+99-38
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,26 @@
1-
idprop_string(idprop::IdProp) = "$(idprop[1]).$(idprop[2])"
1+
dependency_string(dep::Dependency) = "$(dep.id).$(dep.property)"
22

33

4-
function output_string(id::CallbackId)
5-
if length(id.output) == 1
6-
return idprop_string(id.output[1])
4+
5+
function output_string(deps::CallbackDeps)
6+
if deps.multi_out
7+
return ".." *
8+
join(dependency_string.(deps.output), "...") *
9+
".."
710
end
8-
return ".." *
9-
join(map(idprop_string, id.output), "...") *
10-
".."
11+
return dependency_string(deps.output[1])
1112
end
1213

13-
"""
14-
callback!(func::Function, app::Dash, id::CallbackId)
1514

16-
Create a callback that updates the output by calling function `func`.
15+
"""
16+
function callback!(func::Union{Function, ClientsideFunction, String},
17+
app::DashApp,
18+
output::Union{Vector{Output}, Output},
19+
input::Union{Vector{Input}, Input},
20+
state::Union{Vector{State}, State} = []
21+
)
1722
23+
Create a callback that updates the output by calling function `func`.
1824
1925
# Examples
2026
@@ -28,42 +34,97 @@ app = dash() do
2834
2935
end
3036
end
31-
callback!(app, CallbackId(
32-
state = [(:graphTitle, :type)],
33-
input = [(:graphTitle, :value)],
34-
output = [(:outputID, :children), (:outputID2, :children)]
35-
)
36-
) do stateType, inputValue
37+
callback!(app, [Output("outputID2", "children"), Output("outputID", "children")],
38+
Input("graphTitle", "value"),
39+
State("graphTitle", "type")
40+
) do inputValue, stateType
3741
return (stateType * "..." * inputValue, inputValue)
3842
end
3943
```
44+
"""
45+
function callback!(func::Union{Function, ClientsideFunction, String},
46+
app::DashApp,
47+
output::Union{Vector{Output}, Output},
48+
input::Union{Vector{Input}, Input},
49+
state::Union{Vector{State}, State} = State[]
50+
)
51+
return _callback!(func, app, CallbackDeps(output, input, state))
52+
end
53+
54+
"""
55+
function callback!(func::Union{Function, ClientsideFunction, String},
56+
app::DashApp,
57+
deps...
58+
)
4059
41-
Alternatively, the `callid` string macro is also available when passing `input`, `state`, and `output` arguments to `callback!`:
60+
Create a callback that updates the output by calling function `func`.
61+
"Flat" version of `callback!` function, `deps` must be ``Output..., Input...[,State...]``
62+
63+
# Examples
4264
4365
```julia
44-
callback!(app, callid"{graphTitle.type} graphTitle.value => outputID.children, outputID2.children") do stateType, inputValue
66+
app = dash() do
67+
html_div() do
68+
dcc_input(id="graphTitle", value="Let's Dance!", type = "text"),
69+
dcc_input(id="graphTitle2", value="Let's Dance!", type = "text"),
70+
html_div(id="outputID"),
71+
html_div(id="outputID2")
4572
73+
end
74+
end
75+
callback!(app,
76+
Output("outputID2", "children"),
77+
Output("outputID", "children"),
78+
Input("graphTitle", "value"),
79+
State("graphTitle", "type")
80+
) do inputValue, stateType
4681
return (stateType * "..." * inputValue, inputValue)
4782
end
4883
```
84+
"""
85+
function callback!(func::Union{Function, ClientsideFunction, String},
86+
app::DashApp,
87+
deps::Dependency...
88+
)
89+
output = Output[]
90+
input = Input[]
91+
state = State[]
92+
_process_callback_args(deps, (output, input, state))
93+
return _callback!(func, app, CallbackDeps(output, input, state, length(output) > 1))
94+
end
95+
96+
function _process_callback_args(args::Tuple{T, Vararg}, dest::Tuple{Vector{T}, Vararg}) where {T}
97+
push!(dest[1], args[1])
98+
_process_callback_args(Base.tail(args), dest)
99+
end
100+
function _process_callback_args(args::Tuple{}, dest::Tuple{Vector{T}, Vararg}) where {T}
101+
end
102+
function _process_callback_args(args::Tuple, dest::Tuple{Vector{T}, Vararg}) where {T}
103+
_process_callback_args(args, Base.tail(dest))
104+
end
105+
function _process_callback_args(args::Tuple, dest::Tuple{})
106+
error("The callback method must received first all Outputs, then all Inputs, then all States")
107+
end
108+
function _process_callback_args(args::Tuple{}, dest::Tuple{})
109+
end
49110

50111

51-
"""
52-
function callback!(func::Union{Function, ClientsideFunction, String}, app::DashApp, id::CallbackId)
112+
function _callback!(func::Union{Function, ClientsideFunction, String}, app::DashApp, deps::CallbackDeps)
53113

54-
check_callback(func, app, id)
114+
check_callback(func, app, deps)
55115

56-
out_symbol = Symbol(output_string(id))
57-
callback_func = make_callback_func!(app, func, id)
58-
push!(app.callbacks, out_symbol => Callback(callback_func, id))
116+
out_symbol = Symbol(output_string(deps))
117+
callback_func = make_callback_func!(app, func, deps)
118+
push!(app.callbacks, out_symbol => Callback(callback_func, deps))
59119
end
60120

61-
make_callback_func!(app::DashApp, func::Union{Function, ClientsideFunction}, id::CallbackId) = func
62121

63-
function make_callback_func!(app::DashApp, func::String, id::CallbackId)
64-
first_output = first(id.output)
65-
namespace = replace("_dashprivate_$(first_output[1])", "\""=>"\\\"")
66-
function_name = replace("$(first_output[2])", "\""=>"\\\"")
122+
make_callback_func!(app::DashApp, func::Union{Function, ClientsideFunction}, deps::CallbackDeps) = func
123+
124+
function make_callback_func!(app::DashApp, func::String, deps::CallbackDeps)
125+
first_output = first(deps.output)
126+
namespace = replace("_dashprivate_$(first_output.id)", "\""=>"\\\"")
127+
function_name = replace("$(first_output.property)", "\""=>"\\\"")
67128

68129
function_string = """
69130
var clientside = window.dash_clientside = window.dash_clientside || {};
@@ -74,26 +135,26 @@ function make_callback_func!(app::DashApp, func::String, id::CallbackId)
74135
return ClientsideFunction(namespace, function_name)
75136
end
76137

77-
function check_callback(func, app::DashApp, id::CallbackId)
138+
function check_callback(func, app::DashApp, deps::CallbackDeps)
78139

79-
80140

81-
isempty(id.input) && error("The callback method requires that one or more properly formatted inputs are passed.")
141+
isempty(deps.output) && error("The callback method requires that one or more properly formatted outputs are passed.")
142+
isempty(deps.input) && error("The callback method requires that one or more properly formatted inputs are passed.")
82143

83-
length(id.output) != length(unique(id.output)) && error("One or more callback outputs have been duplicated; please confirm that all outputs are unique.")
144+
length(deps.output) != length(unique(deps.output)) && error("One or more callback outputs have been duplicated; please confirm that all outputs are unique.")
84145

85-
for out in id.output
86-
if any(x->out in x.id.output, values(app.callbacks))
146+
for out in deps.output
147+
if any(x->out in x.dependencies.output, values(app.callbacks))
87148
error("output \"$(out)\" already registered")
88149
end
89150
end
90151

91-
args_count = length(id.state) + length(id.input)
152+
args_count = length(deps.state) + length(deps.input)
92153

93154
check_callback_func(func, args_count)
94155

95-
for id_prop in id.input
96-
id_prop in id.output && error("Circular input and output arguments were found. Please verify that callback outputs are not also input arguments.")
156+
for id_prop in deps.input
157+
id_prop in deps.output && error("Circular input and output arguments were found. Please verify that callback outputs are not also input arguments.")
97158
end
98159
end
99160

src/app/supporttypes.jl

+25-11
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,39 @@
1-
const IdProp = Tuple{Symbol, Symbol}
1+
struct TraitInput end
2+
struct TraitOutput end
3+
struct TraitState end
24

3-
struct CallbackId
4-
state ::Vector{IdProp}
5-
input ::Vector{IdProp}
6-
output ::Vector{IdProp}
5+
struct Dependency{T}
6+
id ::String
7+
property ::String
78
end
89

9-
CallbackId(;input,
10-
output,
11-
state = Vector{IdProp}()
12-
) = CallbackId(state, input, output)
10+
const Input = Dependency{TraitInput}
11+
const State = Dependency{TraitState}
12+
const Output = Dependency{TraitOutput}
13+
14+
const IdProp = Tuple{Symbol, Symbol}
15+
16+
17+
18+
struct CallbackDeps
19+
output ::Vector{Output}
20+
input ::Vector{Input}
21+
state ::Vector{State}
22+
multi_out::Bool
23+
CallbackDeps(output, input, state, multi_out) = new(output, input, state, multi_out)
24+
CallbackDeps(output::Output, input, state = State[]) = new(output, input, state, false)
25+
CallbackDeps(output::Vector{Output}, input, state = State[]) = new(output, input, state, true)
26+
end
1327

28+
Base.convert(::Type{Vector{T}}, v::T) where {T<:Dependency}= [v]
1429

15-
Base.convert(::Type{Vector{IdProp}}, v::IdProp) = [v]
1630
struct ClientsideFunction
1731
namespace ::String
1832
function_name ::String
1933
end
2034
struct Callback
2135
func ::Union{Function, ClientsideFunction}
22-
id ::CallbackId
36+
dependencies ::CallbackDeps
2337
end
2438

2539
struct PreventUpdate <: Exception

src/handler/handlers.jl

+8-9
Original file line numberDiff line numberDiff line change
@@ -46,20 +46,20 @@ function _process_callback(app::DashApp, body::String)
4646
return get(x, :value, nothing)
4747
end
4848
args = []
49-
if haskey(params, :state)
50-
append!(args, convert_values(params.state))
51-
end
5249
if haskey(params, :inputs)
5350
append!(args, convert_values(params.inputs))
5451
end
52+
if haskey(params, :state)
53+
append!(args, convert_values(params.state))
54+
end
5555

5656
res = app.callbacks[output].func(args...)
57-
if length(app.callbacks[output].id.output) == 1
57+
if !app.callbacks[output].dependencies.multi_out
5858
if !(res isa NoUpdate)
5959
return Dict(
6060
:response => Dict(
6161
:props => Dict(
62-
Symbol(app.callbacks[output].id.output[1][2]) => Front.to_dash(res)
62+
Symbol(app.callbacks[output].dependencies.output[1].property) => Front.to_dash(res)
6363
)
6464
)
6565
)
@@ -68,11 +68,11 @@ function _process_callback(app::DashApp, body::String)
6868
end
6969
end
7070
response = Dict{Symbol, Any}()
71-
for (ind, out) in enumerate(app.callbacks[output].id.output)
71+
for (ind, out) in enumerate(app.callbacks[output].dependencies.output)
7272
if !(res[ind] isa NoUpdate)
7373
push!(response,
74-
Symbol(out[1]) => Dict(
75-
Symbol(out[2]) => Front.to_dash(res[ind])
74+
Symbol(out.id) => Dict(
75+
Symbol(out.property) => Front.to_dash(res[ind])
7676
)
7777
)
7878
end
@@ -212,7 +212,6 @@ validate_layout(layout) = error("The layout must be a component, tree of compone
212212
#For test purposes, with the ability to pass a custom registry
213213
function make_handler(app::DashApp, registry::ResourcesRegistry; check_layout = false)
214214

215-
216215
state = HandlerState(app, registry)
217216
prefix = get_setting(app, :routes_pathname_prefix)
218217
assets_url_path = get_setting(app, :assets_url_path)

src/handler/state.jl

+6-6
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,12 @@ end
2222
_dep_clientside_func(func::ClientsideFunction) = func
2323
_dep_clientside_func(func) = nothing
2424
function _dependencies_json(app::DashApp)
25-
id_prop_named(p::IdProp) = (id = p[1], property = p[2])
26-
result = map(values(app.callbacks)) do dep
27-
(inputs = id_prop_named.(dep.id.input),
28-
state = id_prop_named.(dep.id.state),
29-
output = output_string(dep.id),
30-
clientside_function = _dep_clientside_func(dep.func)
25+
deps_tuple(p) = (id = p.id, property = p.property)
26+
result = map(values(app.callbacks)) do callback
27+
(inputs = deps_tuple.(callback.dependencies.input),
28+
state = deps_tuple.(callback.dependencies.state),
29+
output = output_string(callback.dependencies),
30+
clientside_function = _dep_clientside_func(callback.func)
3131
)
3232
end
3333
return JSON2.write(result)

src/utils/misc.jl

+1-24
Original file line numberDiff line numberDiff line change
@@ -62,27 +62,4 @@ end
6262

6363
function generate_hash()
6464
return strip(string(UUIDs.uuid4()), '-')
65-
end
66-
67-
"""
68-
@callid_str"
69-
70-
Macro for crating Dash CallbackId.
71-
Parse string in form "[{State1[, ...]}] Input1[, ...] => Output1[, ...]"
72-
73-
#Examples
74-
```julia
75-
id1 = callid"{inputDiv.children} input.value => output1.value, output2.value"
76-
```
77-
"""
78-
macro callid_str(s)
79-
rex = r"(\{(?<state>.*)\})?(?<input>.*)=>(?<output>.*)"ms
80-
m = match(rex, s)
81-
if isnothing(m)
82-
error("expected {state} input => output")
83-
end
84-
input = parse_props(strip(m[:input]))
85-
output = parse_props(strip(m[:output]))
86-
state = isnothing(m[:state]) ? Vector{IdProp}() : parse_props(strip(m[:state]))
87-
return CallbackId(state, input, output)
88-
end
65+
end

0 commit comments

Comments
 (0)