Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

callback! function refactoring #39

Merged
merged 3 commits into from
Jun 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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