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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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