Skip to content

Commit

Permalink
Merge pull request #39 from plotly/callbacks_refactoring
Browse files Browse the repository at this point in the history
callback! function refactoring
  • Loading branch information
waralex authored Jun 8, 2020
2 parents b302856 + 7c36096 commit 308cee3
Show file tree
Hide file tree
Showing 31 changed files with 394 additions and 223 deletions.
19 changes: 9 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,17 +81,15 @@ julia> app.layout = html_div() do
html_div(id = "my-div")
end
julia> callback!(app, callid"my-id.value => my-div.children") do input_value
julia> callback!(app, Output("my-div", "children"), Input("my-id", "value")) do input_value
"You've entered $(input_value)"
end
julia> run_server(app, "0.0.0.0", 8080)
```

* You can make your dashboard interactive by register callbacks for changes in frontend with function ``callback!(func::Function, app::Dash, id::CallbackId)``
* Inputs and outputs (and states, see below) of callback are described by struct `CallbackId` which can easily created by string macro `callid""`
* `callid""` parse string in form ``"[{state1 [,...]}] input1[,...] => output1[,...]"`` where all items is ``"<element id>.<property name>"``
* 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.
* You can make your dashboard interactive by register callbacks for changes in frontend with function ``callback!(func::Function, app::Dash, output, input, state)``
* Inputs and outputs (and states, see below) of callback can be `Input`, `Output`, `State` objects or vectors of this objects
* 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.

### States and Multiple Outputs
```jldoctest
Expand All @@ -107,7 +105,7 @@ julia> app.layout = html_div() do
html_div(id = "my-div2")
end
julia> callback!(app, callid"{my-id.type} my-id.value => my-div.children, my-div2.children") do state_value, input_value
julia> callback!(app, [Output("my-div"."children"), Output("my-div2"."children")], Input("my-id", "value"), State("my-id", "type")) do input_value, state_value
"You've entered $(input_value) in input with type $(state_value)",
"You've entered $(input_value)"
end
Expand Down Expand Up @@ -163,9 +161,10 @@ def update_output(n_clicks, state1, state2):
```
* Dash.jl:
```julia
callback!(app, callid"""{state1.value, state2.value}
submit-button.n_clicks
=> output.children""" ) do state1, state2, n_clicks
callback!(app, Output("output", "children"),
[Input("submit-button", "n_clicks")],
[State("state-1", "value"),
State("state-2", "value")]) do n_clicks, state1, state2
.....
end
```
Expand Down
5 changes: 3 additions & 2 deletions src/Dash.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ include("Front.jl")
import .Front
using .Components

export dash, Component, Front, <|, @callid_str, CallbackId, callback!,
export dash, Component, Front, callback!,
enable_dev_tools!, ClientsideFunction,
run_server, PreventUpdate, no_update, @var_str
run_server, PreventUpdate, no_update, @var_str,
Input, Output, State, make_handler



Expand Down
137 changes: 99 additions & 38 deletions src/app/callbacks.jl
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
idprop_string(idprop::IdProp) = "$(idprop[1]).$(idprop[2])"
dependency_string(dep::Dependency) = "$(dep.id).$(dep.property)"


function output_string(id::CallbackId)
if length(id.output) == 1
return idprop_string(id.output[1])

function output_string(deps::CallbackDeps)
if deps.multi_out
return ".." *
join(dependency_string.(deps.output), "...") *
".."
end
return ".." *
join(map(idprop_string, id.output), "...") *
".."
return dependency_string(deps.output[1])
end

"""
callback!(func::Function, app::Dash, id::CallbackId)

Create a callback that updates the output by calling function `func`.
"""
function callback!(func::Union{Function, ClientsideFunction, String},
app::DashApp,
output::Union{Vector{Output}, Output},
input::Union{Vector{Input}, Input},
state::Union{Vector{State}, State} = []
)
Create a callback that updates the output by calling function `func`.
# Examples
Expand All @@ -28,42 +34,97 @@ app = dash() do
end
end
callback!(app, CallbackId(
state = [(:graphTitle, :type)],
input = [(:graphTitle, :value)],
output = [(:outputID, :children), (:outputID2, :children)]
)
) do stateType, inputValue
callback!(app, [Output("outputID2", "children"), Output("outputID", "children")],
Input("graphTitle", "value"),
State("graphTitle", "type")
) do inputValue, stateType
return (stateType * "..." * inputValue, inputValue)
end
```
"""
function callback!(func::Union{Function, ClientsideFunction, String},
app::DashApp,
output::Union{Vector{Output}, Output},
input::Union{Vector{Input}, Input},
state::Union{Vector{State}, State} = State[]
)
return _callback!(func, app, CallbackDeps(output, input, state))
end

"""
function callback!(func::Union{Function, ClientsideFunction, String},
app::DashApp,
deps...
)
Alternatively, the `callid` string macro is also available when passing `input`, `state`, and `output` arguments to `callback!`:
Create a callback that updates the output by calling function `func`.
"Flat" version of `callback!` function, `deps` must be ``Output..., Input...[,State...]``
# Examples
```julia
callback!(app, callid"{graphTitle.type} graphTitle.value => outputID.children, outputID2.children") do stateType, inputValue
app = dash() do
html_div() do
dcc_input(id="graphTitle", value="Let's Dance!", type = "text"),
dcc_input(id="graphTitle2", value="Let's Dance!", type = "text"),
html_div(id="outputID"),
html_div(id="outputID2")
end
end
callback!(app,
Output("outputID2", "children"),
Output("outputID", "children"),
Input("graphTitle", "value"),
State("graphTitle", "type")
) do inputValue, stateType
return (stateType * "..." * inputValue, inputValue)
end
```
"""
function callback!(func::Union{Function, ClientsideFunction, String},
app::DashApp,
deps::Dependency...
)
output = Output[]
input = Input[]
state = State[]
_process_callback_args(deps, (output, input, state))
return _callback!(func, app, CallbackDeps(output, input, state, length(output) > 1))
end

function _process_callback_args(args::Tuple{T, Vararg}, dest::Tuple{Vector{T}, Vararg}) where {T}
push!(dest[1], args[1])
_process_callback_args(Base.tail(args), dest)
end
function _process_callback_args(args::Tuple{}, dest::Tuple{Vector{T}, Vararg}) where {T}
end
function _process_callback_args(args::Tuple, dest::Tuple{Vector{T}, Vararg}) where {T}
_process_callback_args(args, Base.tail(dest))
end
function _process_callback_args(args::Tuple, dest::Tuple{})
error("The callback method must received first all Outputs, then all Inputs, then all States")
end
function _process_callback_args(args::Tuple{}, dest::Tuple{})
end


"""
function callback!(func::Union{Function, ClientsideFunction, String}, app::DashApp, id::CallbackId)
function _callback!(func::Union{Function, ClientsideFunction, String}, app::DashApp, deps::CallbackDeps)

check_callback(func, app, id)
check_callback(func, app, deps)

out_symbol = Symbol(output_string(id))
callback_func = make_callback_func!(app, func, id)
push!(app.callbacks, out_symbol => Callback(callback_func, id))
out_symbol = Symbol(output_string(deps))
callback_func = make_callback_func!(app, func, deps)
push!(app.callbacks, out_symbol => Callback(callback_func, deps))
end

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

function make_callback_func!(app::DashApp, func::String, id::CallbackId)
first_output = first(id.output)
namespace = replace("_dashprivate_$(first_output[1])", "\""=>"\\\"")
function_name = replace("$(first_output[2])", "\""=>"\\\"")
make_callback_func!(app::DashApp, func::Union{Function, ClientsideFunction}, deps::CallbackDeps) = func

function make_callback_func!(app::DashApp, func::String, deps::CallbackDeps)
first_output = first(deps.output)
namespace = replace("_dashprivate_$(first_output.id)", "\""=>"\\\"")
function_name = replace("$(first_output.property)", "\""=>"\\\"")

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

function check_callback(func, app::DashApp, id::CallbackId)
function check_callback(func, app::DashApp, deps::CallbackDeps)



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

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

for out in id.output
if any(x->out in x.id.output, values(app.callbacks))
for out in deps.output
if any(x->out in x.dependencies.output, values(app.callbacks))
error("output \"$(out)\" already registered")
end
end

args_count = length(id.state) + length(id.input)
args_count = length(deps.state) + length(deps.input)

check_callback_func(func, args_count)

for id_prop in id.input
id_prop in id.output && error("Circular input and output arguments were found. Please verify that callback outputs are not also input arguments.")
for id_prop in deps.input
id_prop in deps.output && error("Circular input and output arguments were found. Please verify that callback outputs are not also input arguments.")
end
end

Expand Down
36 changes: 25 additions & 11 deletions src/app/supporttypes.jl
Original file line number Diff line number Diff line change
@@ -1,25 +1,39 @@
const IdProp = Tuple{Symbol, Symbol}
struct TraitInput end
struct TraitOutput end
struct TraitState end

struct CallbackId
state ::Vector{IdProp}
input ::Vector{IdProp}
output ::Vector{IdProp}
struct Dependency{T}
id ::String
property ::String
end

CallbackId(;input,
output,
state = Vector{IdProp}()
) = CallbackId(state, input, output)
const Input = Dependency{TraitInput}
const State = Dependency{TraitState}
const Output = Dependency{TraitOutput}

const IdProp = Tuple{Symbol, Symbol}



struct CallbackDeps
output ::Vector{Output}
input ::Vector{Input}
state ::Vector{State}
multi_out::Bool
CallbackDeps(output, input, state, multi_out) = new(output, input, state, multi_out)
CallbackDeps(output::Output, input, state = State[]) = new(output, input, state, false)
CallbackDeps(output::Vector{Output}, input, state = State[]) = new(output, input, state, true)
end

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

Base.convert(::Type{Vector{IdProp}}, v::IdProp) = [v]
struct ClientsideFunction
namespace ::String
function_name ::String
end
struct Callback
func ::Union{Function, ClientsideFunction}
id ::CallbackId
dependencies ::CallbackDeps
end

struct PreventUpdate <: Exception
Expand Down
17 changes: 8 additions & 9 deletions src/handler/handlers.jl
Original file line number Diff line number Diff line change
Expand Up @@ -46,20 +46,20 @@ function _process_callback(app::DashApp, body::String)
return get(x, :value, nothing)
end
args = []
if haskey(params, :state)
append!(args, convert_values(params.state))
end
if haskey(params, :inputs)
append!(args, convert_values(params.inputs))
end
if haskey(params, :state)
append!(args, convert_values(params.state))
end

res = app.callbacks[output].func(args...)
if length(app.callbacks[output].id.output) == 1
if !app.callbacks[output].dependencies.multi_out
if !(res isa NoUpdate)
return Dict(
:response => Dict(
:props => Dict(
Symbol(app.callbacks[output].id.output[1][2]) => Front.to_dash(res)
Symbol(app.callbacks[output].dependencies.output[1].property) => Front.to_dash(res)
)
)
)
Expand All @@ -68,11 +68,11 @@ function _process_callback(app::DashApp, body::String)
end
end
response = Dict{Symbol, Any}()
for (ind, out) in enumerate(app.callbacks[output].id.output)
for (ind, out) in enumerate(app.callbacks[output].dependencies.output)
if !(res[ind] isa NoUpdate)
push!(response,
Symbol(out[1]) => Dict(
Symbol(out[2]) => Front.to_dash(res[ind])
Symbol(out.id) => Dict(
Symbol(out.property) => Front.to_dash(res[ind])
)
)
end
Expand Down Expand Up @@ -212,7 +212,6 @@ validate_layout(layout) = error("The layout must be a component, tree of compone
#For test purposes, with the ability to pass a custom registry
function make_handler(app::DashApp, registry::ResourcesRegistry; check_layout = false)


state = HandlerState(app, registry)
prefix = get_setting(app, :routes_pathname_prefix)
assets_url_path = get_setting(app, :assets_url_path)
Expand Down
12 changes: 6 additions & 6 deletions src/handler/state.jl
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ end
_dep_clientside_func(func::ClientsideFunction) = func
_dep_clientside_func(func) = nothing
function _dependencies_json(app::DashApp)
id_prop_named(p::IdProp) = (id = p[1], property = p[2])
result = map(values(app.callbacks)) do dep
(inputs = id_prop_named.(dep.id.input),
state = id_prop_named.(dep.id.state),
output = output_string(dep.id),
clientside_function = _dep_clientside_func(dep.func)
deps_tuple(p) = (id = p.id, property = p.property)
result = map(values(app.callbacks)) do callback
(inputs = deps_tuple.(callback.dependencies.input),
state = deps_tuple.(callback.dependencies.state),
output = output_string(callback.dependencies),
clientside_function = _dep_clientside_func(callback.func)
)
end
return JSON2.write(result)
Expand Down
25 changes: 1 addition & 24 deletions src/utils/misc.jl
Original file line number Diff line number Diff line change
Expand Up @@ -62,27 +62,4 @@ end

function generate_hash()
return strip(string(UUIDs.uuid4()), '-')
end

"""
@callid_str"
Macro for crating Dash CallbackId.
Parse string in form "[{State1[, ...]}] Input1[, ...] => Output1[, ...]"
#Examples
```julia
id1 = callid"{inputDiv.children} input.value => output1.value, output2.value"
```
"""
macro callid_str(s)
rex = r"(\{(?<state>.*)\})?(?<input>.*)=>(?<output>.*)"ms
m = match(rex, s)
if isnothing(m)
error("expected {state} input => output")
end
input = parse_props(strip(m[:input]))
output = parse_props(strip(m[:output]))
state = isnothing(m[:state]) ? Vector{IdProp}() : parse_props(strip(m[:state]))
return CallbackId(state, input, output)
end
end
Loading

0 comments on commit 308cee3

Please sign in to comment.