From f376132b3af923bde0f7e8af8ea35e5e4de38d08 Mon Sep 17 00:00:00 2001 From: Alexandr Romanenko Date: Fri, 29 May 2020 11:22:35 +0300 Subject: [PATCH 1/2] callback! refactoring --- README.md | 18 ++-- src/Dash.jl | 5 +- src/app/callbacks.jl | 84 ++++++++--------- src/app/supporttypes.jl | 34 ++++--- src/handler/handlers.jl | 16 ++-- src/handler/state.jl | 12 +-- src/utils/misc.jl | 25 +----- test/callbacks.jl | 89 ++++++++----------- test/core.jl | 19 +--- .../jlcbsc001_simple_callback.jl | 7 +- ...jlcbsc002_callbacks_generating_children.jl | 4 +- ..._callback_with_unloaded_async_component.jl | 2 +- .../jlcbsc005_children_types.jl | 2 +- .../jlcbsc006_multiple_outputs.jl | 2 +- .../jlcbsc007_prevent_update.jl | 5 +- .../jlcbsc008_prevent_update_multiple.jl | 5 +- ...1_simple_clientside_serverside_callback.jl | 7 +- ...chained_serverside_clientside_callbacks.jl | 35 +++----- ...side_exceptions_halt_subsequent_updates.jl | 4 +- .../jlclsd004_clientside_multiple_outputs.jl | 4 +- ...ientside_fails_when_returning_a_promise.jl | 2 +- .../jl_clientside/jlclsd006_PreventUpdate.jl | 4 +- .../jl_clientside/jlclsd007_no_update.jl | 3 +- .../jlclsd008_clientside_inline_source.jl | 4 +- .../jl_dash_assets/jldada001_assets.jl | 7 +- .../jl_hot_reload/jldvhr002_hot_restart.jl | 2 +- .../jldvpc001_prop_check_errors_with_path.jl | 2 +- test/integration/devtools/test_hot_reload.py | 2 +- test/integration/test_render.py | 7 +- 29 files changed, 187 insertions(+), 225 deletions(-) diff --git a/README.md b/README.md index 1d0de2a..9b4b9bc 100644 --- a/README.md +++ b/README.md @@ -73,16 +73,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 ``"."`` -* Callback function 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 @@ -95,7 +94,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 @@ -151,9 +150,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 ``` diff --git a/src/Dash.jl b/src/Dash.jl index f5e6fc1..0d79d85 100644 --- a/src/Dash.jl +++ b/src/Dash.jl @@ -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 diff --git a/src/app/callbacks.jl b/src/app/callbacks.jl index e2fa1be..79edff1 100644 --- a/src/app/callbacks.jl +++ b/src/app/callbacks.jl @@ -1,19 +1,25 @@ -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 length(deps.output) == 1 + return dependency_string(deps.output[1]) end return ".." * - join(map(idprop_string, id.output), "...") * + join(dependency_string.(deps.output), "...") * ".." end + """ - callback!(func::Function, app::Dash, id::CallbackId) + 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`. +Create a callback that updates the output by calling function `func`. # Examples @@ -28,42 +34,39 @@ app = dash() do end end -callback!(app, CallbackId( - state = [(:graphTitle, :type)], - input = [(:graphTitle, :value)], - output = [(:outputID, :children), (:outputID2, :children)] - ) +callback!(app, [Output("outputID2", "children"), Output("outputID", "children")], + Input("graphTitle", "value"), + State("graphTitle", "type") ) do stateType, inputValue return (stateType * "..." * inputValue, inputValue) end ``` - -Alternatively, the `callid` string macro is also available when passing `input`, `state`, and `output` arguments to `callback!`: - -```julia -callback!(app, callid"{graphTitle.type} graphTitle.value => outputID.children, outputID2.children") do stateType, inputValue - - return (stateType * "..." * inputValue, inputValue) +""" +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, 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 || {}; @@ -74,26 +77,25 @@ 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.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 diff --git a/src/app/supporttypes.jl b/src/app/supporttypes.jl index c19d3fd..61258a3 100644 --- a/src/app/supporttypes.jl +++ b/src/app/supporttypes.jl @@ -1,25 +1,37 @@ -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} + CallbackDeps(output, input, state = State[]) = new(output, input, state) + CallbackDeps(;output, input, state = State[]) = new(output, input, state) +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 diff --git a/src/handler/handlers.jl b/src/handler/handlers.jl index a29a6df..97502a6 100644 --- a/src/handler/handlers.jl +++ b/src/handler/handlers.jl @@ -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 length(app.callbacks[output].dependencies.output) == 1 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) ) ) ) @@ -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 diff --git a/src/handler/state.jl b/src/handler/state.jl index 128dcdf..d845b8f 100644 --- a/src/handler/state.jl +++ b/src/handler/state.jl @@ -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) diff --git a/src/utils/misc.jl b/src/utils/misc.jl index 029ece1..6028243 100644 --- a/src/utils/misc.jl +++ b/src/utils/misc.jl @@ -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"(\{(?.*)\})?(?.*)=>(?.*)"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 \ No newline at end of file diff --git a/test/callbacks.jl b/test/callbacks.jl index 49104a6..5dcb588 100644 --- a/test/callbacks.jl +++ b/test/callbacks.jl @@ -2,27 +2,7 @@ import HTTP, JSON2 using Test using Dash -@testset "callid" begin - id = callid"id1.prop1 => id2.prop2" - @test id isa CallbackId - @test length(id.state) == 0 - @test length(id.input) == 1 - @test length(id.output) == 1 - @test id.input[1] == (:id1, :prop1) - @test id.output[1] == (:id2, :prop2) - - id = callid"{state1.prop1, state2.prop2} input1.prop1, input2.prop2 => output1.prop1, output2.prop2" - @test id isa CallbackId - @test length(id.state) == 2 - @test length(id.input) == 2 - @test length(id.output) == 2 - @test id.input[1] == (:input1, :prop1) - @test id.input[2] == (:input2, :prop2) - @test id.output[1] == (:output1, :prop1) - @test id.output[2] == (:output2, :prop2) - @test id.state[1] == (:state1, :prop1) - @test id.state[2] == (:state2, :prop2) -end + @testset "callback!" begin app = dash() @@ -31,7 +11,7 @@ end html_div(id = "my-div") end - callback!(app, callid"my-id.value => my-div.children") do value + callback!(app, Output("my-div", "children"), Input("my-id", "value")) do value return value end @test length(app.callbacks) == 1 @@ -57,8 +37,8 @@ end html_div(id = "my-div"), html_div(id = "my-div2") end - callback!(app, callid"{my-id.type} my-id.value => my-div.children, my-div2.children") do state, value - return state, value + callback!(app, [Output("my-div","children"), Output("my-div2","children")], Input("my-id","value"), State("my-id","type")) do value, state + return value, state end @test length(app.callbacks) == 1 @test haskey(app.callbacks, Symbol("..my-div.children...my-div2.children..")) @@ -72,10 +52,10 @@ end html_div(id = "my-div2") end - callback!(app, callid"my-id.value => my-div.children") do value + callback!(app, Output("my-div","children"), Input("my-id","value")) do value return value end - callback!(app, callid"my-id.value => my-div2.children") do value + callback!(app, Output("my-div2","children"), Input("my-id","value")) do value return "v_$(value)" end @@ -85,46 +65,50 @@ end @test app.callbacks[Symbol("my-div.children")].func("value") == "value" @test app.callbacks[Symbol("my-div2.children")].func("value") == "v_value" - @test_throws ErrorException callback!(app, callid"my-id.value => my-div2.children") do value + #output already exists + @test_throws ErrorException callback!(app, Output("my-div2","children"), Input("my-id","value")) do value return "v_$(value)" end - @test_throws ErrorException callback!(app, callid"{my-id.value} my-id.value => my-id.value") do value + #output same as input + @test_throws ErrorException callback!(app, + Output("my-id","value"), + Input("my-id","value"), + State("my-id","value")) do value return "v_$(value)" end - @test_throws ErrorException callback!(app, callid"my-div.children, my-id.value => my-id.value") do value + #output same as input + @test_throws ErrorException callback!(app, + Output("my-id","value"), + [Input("my-id","value"), Input("my-div","children")]) do value return "v_$(value)" end - @test_throws ErrorException callback!(app, callid"my-id.value => my-div.children, my-id.value") do value + + #output same as input + @test_throws ErrorException callback!(app, + [Output("my-id","value"), Output("my-div","children")], + Input("my-id","value")) do value return "v_$(value)" end - @test_throws ErrorException callback!(app, callid" => my-div2.title, my-id.value") do value + #empty input + @test_throws ErrorException callback!(app, + [Output("my-id","value"), Output("my-div","children")], + Input[]) do value return "v_$(value)" end - @test_throws ErrorException callback!(app, callid"my-id.value => my-div2.title, my-div2.title") do value + + #duplicated output + @test_throws ErrorException callback!(app, + [Output("my-div","value"), Output("my-div","value")], + Input("my-id","value")) do value return "v_$(value)" end - @test_throws ErrorException callback!(app, callid"my-id.value => my-div2.title") do - return "v_" - end - app = dash() - app.layout = html_div() do - dcc_input(id = "my-id", value="initial value", type = "text"), - html_div("test2", id = "my-div"), - html_div(id = "my-div2") do - html_h1("gggg", id = "my-h") - end - end - callback!(app, callid"{my-id.type} my-id.value => my-div.children, my-h.children") do state, value - return state, value - end - @test length(app.callbacks) == 1 end @testset "clientside callbacks function" begin @@ -134,7 +118,7 @@ end html_div(id = "my-div") end - callback!(ClientsideFunction("namespace", "func_name"),app, callid"my-id.value => my-div.children") + callback!(ClientsideFunction("namespace", "func_name"),app, Output("my-div","children"), Input("my-id","value")) @test length(app.callbacks) == 1 @test haskey(app.callbacks, Symbol("my-div.children")) @@ -142,9 +126,6 @@ end @test app.callbacks[Symbol("my-div.children")].func.namespace == "namespace" @test app.callbacks[Symbol("my-div.children")].func.function_name == "func_name" - @test_throws ErrorException callback!(app, callid"my-id.value => my-div.children") do value - return "v_$(value)" - end handler = make_handler(app) request = HTTP.Request("GET", "/_dash-dependencies") @@ -175,7 +156,9 @@ end ); } """ - , app, callid"my-id.value => my-div.children" + , app, + Output("my-div", "children"), + Input("my-id", "value") ) @test length(app.callbacks) == 1 @@ -205,4 +188,4 @@ end body = String(resp.body) @test occursin("clientside[\"_dashprivate_my-div\"]", body) @test occursin("ns[\"children\"]", body) -end \ No newline at end of file +end diff --git a/test/core.jl b/test/core.jl index 6563bc6..790c2ae 100644 --- a/test/core.jl +++ b/test/core.jl @@ -39,10 +39,10 @@ end html_div(id = "my-div"), html_div(id = "my-div2") end - callback!(app, callid"my-id.value => my-div.children") do value + callback!(app, Output("my-div","children"), Input("my-id","value")) do value return value end - callback!(app, callid"my-id.value => my-div2.children") do value + callback!(app, Output("my-div2","children"), Input("my-id","value")) do value return "v_$(value)" end @@ -140,7 +140,7 @@ end html_div(10, id = "my-id"), html_div(id = "my-div") end - callback!(app, callid"my-id.children => my-div.children") do value + callback!(app, Output("my-div","children"), Input("my-id","children")) do value throw(PreventUpdate()) end @@ -159,7 +159,7 @@ end html_div(id = "my-div"), html_div(id = "my-div2") end - callback!(app, callid"my-id.children => my-div.children, my-div2.children") do value + callback!(app, [Output("my-div","children"), Output("my-div2","children")], Input("my-id","children")) do value no_update(), "test" end @@ -172,17 +172,6 @@ end @test result[:response][Symbol("my-div2")][:children] == "test" end -@testset "wildprops" begin - app = dash(external_stylesheets=["test.css"]) - - app.layout = html_div() do - html_div(;id = "my-div", var"data-attr" = "ffff"), - html_div(;id = "my-div2", var"aria-attr" = "gggg") - end - callback!(app, callid"my-div.children => my-div2.aria-attr") do v - - end -end @testset "HTTP Compression" begin # test compression of assets diff --git a/test/integration/callbacks/jl_basic_callback/jlcbsc001_simple_callback.jl b/test/integration/callbacks/jl_basic_callback/jlcbsc001_simple_callback.jl index 6f2f123..70d3276 100644 --- a/test/integration/callbacks/jl_basic_callback/jlcbsc001_simple_callback.jl +++ b/test/integration/callbacks/jl_basic_callback/jlcbsc001_simple_callback.jl @@ -18,10 +18,9 @@ app.layout = html_div() do end end call_count = 0 -callback!(app, CallbackId( - input = [(:input, :value)], - output = [(:output, :children)] - ) +callback!(app, + Output("output","children"), + Input("input","value") ) do value return value end diff --git a/test/integration/callbacks/jl_basic_callback/jlcbsc002_callbacks_generating_children.jl b/test/integration/callbacks/jl_basic_callback/jlcbsc002_callbacks_generating_children.jl index c200638..2ae5292 100644 --- a/test/integration/callbacks/jl_basic_callback/jlcbsc002_callbacks_generating_children.jl +++ b/test/integration/callbacks/jl_basic_callback/jlcbsc002_callbacks_generating_children.jl @@ -8,14 +8,14 @@ app.layout = html_div() do dcc_input(id="input", value = "initial value"), html_div(id="output") end -callback!(app, callid"input.value => output.children") do input +callback!(app, Output("output","children"), Input("input","value")) do input return html_div() do dcc_input(id = "sub-input-1", value = "sub input initial value"), html_div(id = "sub-output-1") end end -callback!(app, callid"sub-input-1.value => sub-output-1.children") do value +callback!(app, Output("sub-output-1","children"), Input("sub-input-1","value")) do value return value end diff --git a/test/integration/callbacks/jl_basic_callback/jlcbsc003_callback_with_unloaded_async_component.jl b/test/integration/callbacks/jl_basic_callback/jlcbsc003_callback_with_unloaded_async_component.jl index c173578..a6ee40b 100644 --- a/test/integration/callbacks/jl_basic_callback/jlcbsc003_callback_with_unloaded_async_component.jl +++ b/test/integration/callbacks/jl_basic_callback/jlcbsc003_callback_with_unloaded_async_component.jl @@ -16,7 +16,7 @@ app.layout = html_div() do end end -callback!(app, callid"btn.n_clicks => output.children") do n_clicks +callback!(app, Output("output","children"), Input("btn","n_clicks")) do n_clicks isnothing(n_clicks) && throw(PreventUpdate()) return "Bye" end diff --git a/test/integration/callbacks/jl_basic_callback/jlcbsc005_children_types.jl b/test/integration/callbacks/jl_basic_callback/jlcbsc005_children_types.jl index d84f984..a6c2cc7 100644 --- a/test/integration/callbacks/jl_basic_callback/jlcbsc005_children_types.jl +++ b/test/integration/callbacks/jl_basic_callback/jlcbsc005_children_types.jl @@ -19,7 +19,7 @@ app.layout = html_div() do html_div(id = "out", children = "init") end -callback!(app, callid"btn.n_clicks => out.children") do n_clicks +callback!(app, Output("out","children"), Input("btn","n_clicks")) do n_clicks (isnothing(n_clicks) || n_clicks > length(outputs)) && throw(PreventUpdate()) return outputs[n_clicks][1] end diff --git a/test/integration/callbacks/jl_basic_callback/jlcbsc006_multiple_outputs.jl b/test/integration/callbacks/jl_basic_callback/jlcbsc006_multiple_outputs.jl index 2b8dda0..a97302c 100644 --- a/test/integration/callbacks/jl_basic_callback/jlcbsc006_multiple_outputs.jl +++ b/test/integration/callbacks/jl_basic_callback/jlcbsc006_multiple_outputs.jl @@ -9,7 +9,7 @@ app.layout = html_div() do html_div(id="output1"), html_div(id="output2") end -callback!(app, callid"input.value => output1.children, output2.children") do input +callback!(app, [Output("output1","children"), Output("output2","children")], Input("input","value")) do input return ("$input first", "$input second") end diff --git a/test/integration/callbacks/jl_basic_callback/jlcbsc007_prevent_update.jl b/test/integration/callbacks/jl_basic_callback/jlcbsc007_prevent_update.jl index b111672..fe2c1d2 100644 --- a/test/integration/callbacks/jl_basic_callback/jlcbsc007_prevent_update.jl +++ b/test/integration/callbacks/jl_basic_callback/jlcbsc007_prevent_update.jl @@ -15,10 +15,11 @@ app.layout = html_div() do html_div(id="output"), html_div(id="regular_output") end -callback!(app, callid"input.value => regular_output.children") do input +callback!(app, Output("regular_output","children"), Input("input","value")) do input return input end -callback!(app, callid"input.value => output.children") do input + +callback!(app, Output("output","children"), Input("input","value")) do input input == "prevent" && throw(PreventUpdate()) input == "no_update" && return no_update() return input diff --git a/test/integration/callbacks/jl_basic_callback/jlcbsc008_prevent_update_multiple.jl b/test/integration/callbacks/jl_basic_callback/jlcbsc008_prevent_update_multiple.jl index 7af4859..f816189 100644 --- a/test/integration/callbacks/jl_basic_callback/jlcbsc008_prevent_update_multiple.jl +++ b/test/integration/callbacks/jl_basic_callback/jlcbsc008_prevent_update_multiple.jl @@ -18,10 +18,11 @@ app.layout = html_div() do html_div(id="output2"), html_div(id="regular_output") end -callback!(app, callid"input.value => regular_output.children") do input +callback!(app, Output("regular_output","children"), Input("input","value")) do input return input end -callback!(app, callid"input.value => output1.children, output2.children") do input + +callback!(app, [Output("output1","children"), Output("output2","children")], Input("input","value")) do input input == "prevent" && throw(PreventUpdate()) if input == "no_update1" return (no_update(), input) diff --git a/test/integration/clientside/jl_clientside/jlclsd001_simple_clientside_serverside_callback.jl b/test/integration/clientside/jl_clientside/jlclsd001_simple_clientside_serverside_callback.jl index ce28e87..d368f91 100644 --- a/test/integration/clientside/jl_clientside/jlclsd001_simple_clientside_serverside_callback.jl +++ b/test/integration/clientside/jl_clientside/jlclsd001_simple_clientside_serverside_callback.jl @@ -10,12 +10,15 @@ app.layout = html_div() do html_div(id="output-serverside") end -callback!(app, callid"input.value=>output-serverside.children") do value +callback!(app, Output("output-serverside", "children"), Input("input","value")) do value return "Server says \"$(value)\"" end callback!( ClientsideFunction("clientside", "display"), - app, callid"input.value=>output-clientside.children") + app, + Output("output-clientside", "children"), + Input("input", "value") +) run_server(app) \ No newline at end of file diff --git a/test/integration/clientside/jl_clientside/jlclsd002_chained_serverside_clientside_callbacks.jl b/test/integration/clientside/jl_clientside/jlclsd002_chained_serverside_clientside_callbacks.jl index 21fb8a8..6143af3 100644 --- a/test/integration/clientside/jl_clientside/jlclsd002_chained_serverside_clientside_callbacks.jl +++ b/test/integration/clientside/jl_clientside/jlclsd002_chained_serverside_clientside_callbacks.jl @@ -23,34 +23,27 @@ end callback!( ClientsideFunction("clientside", "add"), - app, callid""" - x.value, - y.value, - => x-plus-y.value - """) -callback!(app, callid"x-plus-y.value => x-plus-y-div-2.value") do value + app, + Output("x-plus-y", "value"), + [Input("x","value"), Input("y","value")] + ) + +callback!(app, Output("x-plus-y-div-2", "value"), Input("x-plus-y", "value")) do value return Float64(value) / 2. end -callback!(app, callid""" - x.value, - y.value, - x-plus-y.value, - x-plus-y-div-2.value - => display-all-of-the-values.value - """ - ) do args... +callback!(app, + Output("display-all-of-the-values", "value"), + [Input("x","value"), Input("y","value"), Input("x-plus-y","value"), Input("x-plus-y-div-2","value")] + ) do args... return join(string.(args), "\n") end callback!( ClientsideFunction("clientside", "mean"), - app, callid""" - x.value, - y.value, - x-plus-y.value, - x-plus-y-div-2.value - => mean-of-all-values.value - """) + app, + Output("mean-of-all-values","value"), + [Input("x","value"), Input("y","value"), Input("x-plus-y","value"), Input("x-plus-y-div-2","value")] +) run_server(app) diff --git a/test/integration/clientside/jl_clientside/jlclsd003_clientside_exceptions_halt_subsequent_updates.jl b/test/integration/clientside/jl_clientside/jlclsd003_clientside_exceptions_halt_subsequent_updates.jl index c2e6697..a9ff10a 100644 --- a/test/integration/clientside/jl_clientside/jlclsd003_clientside_exceptions_halt_subsequent_updates.jl +++ b/test/integration/clientside/jl_clientside/jlclsd003_clientside_exceptions_halt_subsequent_updates.jl @@ -12,12 +12,12 @@ end callback!( ClientsideFunction("clientside", "add1_break_at_11"), - app, callid"first.value=>second.value" + app, Output("second", "value"), Input("first", "value") ) callback!( ClientsideFunction("clientside", "add1_break_at_11"), - app, callid"second.value=>third.value" + app, Output("third", "value"), Input("second","value") ) run_server(app) \ No newline at end of file diff --git a/test/integration/clientside/jl_clientside/jlclsd004_clientside_multiple_outputs.jl b/test/integration/clientside/jl_clientside/jlclsd004_clientside_multiple_outputs.jl index 59354bc..633477a 100644 --- a/test/integration/clientside/jl_clientside/jlclsd004_clientside_multiple_outputs.jl +++ b/test/integration/clientside/jl_clientside/jlclsd004_clientside_multiple_outputs.jl @@ -14,7 +14,9 @@ end callback!( ClientsideFunction("clientside", "add_to_four_outputs"), - app, callid"input.value=>output-1.value, output-2.value, output-3.value, output-4.value" + app, + [Output("output-1","value"), Output("output-2","value"), Output("output-3","value"), Output("output-4","value")], + Input("input","value") ) diff --git a/test/integration/clientside/jl_clientside/jlclsd005_clientside_fails_when_returning_a_promise.jl b/test/integration/clientside/jl_clientside/jlclsd005_clientside_fails_when_returning_a_promise.jl index 62375a3..440f23a 100644 --- a/test/integration/clientside/jl_clientside/jlclsd005_clientside_fails_when_returning_a_promise.jl +++ b/test/integration/clientside/jl_clientside/jlclsd005_clientside_fails_when_returning_a_promise.jl @@ -12,7 +12,7 @@ end callback!( ClientsideFunction("clientside", "side_effect_and_return_a_promise"), - app, callid"input.children=>output.children" + app, Output("output","children"), Input("input","children") ) diff --git a/test/integration/clientside/jl_clientside/jlclsd006_PreventUpdate.jl b/test/integration/clientside/jl_clientside/jlclsd006_PreventUpdate.jl index 04280f8..f38ed05 100644 --- a/test/integration/clientside/jl_clientside/jlclsd006_PreventUpdate.jl +++ b/test/integration/clientside/jl_clientside/jlclsd006_PreventUpdate.jl @@ -12,12 +12,12 @@ end callback!( ClientsideFunction("clientside", "add1_prevent_at_11"), - app, callid"{third.value} first.value=>second.value" + app, Output("second","value"), Input("first","value"), State("third","value") ) callback!( ClientsideFunction("clientside", "add1_prevent_at_11"), - app, callid"{third.value} second.value=>third.value" + app, Output("third","value"), Input("second","value"), State("third","value") ) diff --git a/test/integration/clientside/jl_clientside/jlclsd007_no_update.jl b/test/integration/clientside/jl_clientside/jlclsd007_no_update.jl index 5508208..25c987f 100644 --- a/test/integration/clientside/jl_clientside/jlclsd007_no_update.jl +++ b/test/integration/clientside/jl_clientside/jlclsd007_no_update.jl @@ -12,7 +12,8 @@ end callback!( ClientsideFunction("clientside", "add1_no_update_at_11"), - app, callid"{second.value, third.value} first.value=>second.value, third.value" + app, [Output("second","value"), Output("third","value")], Input("first","value"), + [State("second","value"), State("third","value")] ) diff --git a/test/integration/clientside/jl_clientside/jlclsd008_clientside_inline_source.jl b/test/integration/clientside/jl_clientside/jlclsd008_clientside_inline_source.jl index 018b2cc..719de06 100644 --- a/test/integration/clientside/jl_clientside/jlclsd008_clientside_inline_source.jl +++ b/test/integration/clientside/jl_clientside/jlclsd008_clientside_inline_source.jl @@ -10,7 +10,7 @@ app.layout = html_div() do html_div(id="output-serverside") end -callback!(app, callid"input.value=>output-serverside.children") do value +callback!(app, Output("output-serverside","children"), Input("input","value")) do value return "Server says \"$(value)\"" end @@ -20,6 +20,6 @@ callback!( return 'Client says "' + value + '"'; } """, - app, callid"input.value=>output-clientside.children") + app, Output("output-clientside","children"), Input("input","value") ) run_server(app) \ No newline at end of file diff --git a/test/integration/dash_assets/jl_dash_assets/jldada001_assets.jl b/test/integration/dash_assets/jl_dash_assets/jldada001_assets.jl index 2e890e9..28e06e5 100644 --- a/test/integration/dash_assets/jl_dash_assets/jldada001_assets.jl +++ b/test/integration/dash_assets/jl_dash_assets/jldada001_assets.jl @@ -24,10 +24,9 @@ app.layout = html_div(id="layout") do html_div("Content", id="content"), dcc_input(id="test") end -callback!(app, CallbackId( - input = [(:input, :value)], - output = [(:output, :children)] - ) +callback!(app, + Output("output","children"), + Input("input","value") ) do value return value end diff --git a/test/integration/devtools/jl_hot_reload/jldvhr002_hot_restart.jl b/test/integration/devtools/jl_hot_reload/jldvhr002_hot_restart.jl index bd0c30a..2f82733 100644 --- a/test/integration/devtools/jl_hot_reload/jldvhr002_hot_restart.jl +++ b/test/integration/devtools/jl_hot_reload/jldvhr002_hot_restart.jl @@ -9,7 +9,7 @@ app.layout = html_div(id="before-reload-content") do html_div(id="output") end -callback!(app, callid"input.value => output.children") do value +callback!(app, Output("output","children"), Input("input","value")) do value return "before reload $value" end diff --git a/test/integration/devtools/jl_props_check/jldvpc001_prop_check_errors_with_path.jl b/test/integration/devtools/jl_props_check/jldvpc001_prop_check_errors_with_path.jl index 444c813..0b902ff 100644 --- a/test/integration/devtools/jl_props_check/jldvpc001_prop_check_errors_with_path.jl +++ b/test/integration/devtools/jl_props_check/jldvpc001_prop_check_errors_with_path.jl @@ -131,7 +131,7 @@ app.layout = html_div() do dcc_location(id="location") end -callback!(app, callid"location.pathname => content.children") do pathname +callback!(app, Output("content","children"), Input("location","pathname")) do pathname if isnothing(pathname) || pathname == "/" return "Initial state" end diff --git a/test/integration/devtools/test_hot_reload.py b/test/integration/devtools/test_hot_reload.py index d5d74f0..cbe3533 100644 --- a/test/integration/devtools/test_hot_reload.py +++ b/test/integration/devtools/test_hot_reload.py @@ -27,7 +27,7 @@ html_div(id="output") end -callback!(app, callid"input.value => output.children") do value +callback!(app, Output("output","children"), Input("input","value")) do value return "after reload $value" end diff --git a/test/integration/test_render.py b/test/integration/test_render.py index 788edc7..8b5b578 100644 --- a/test/integration/test_render.py +++ b/test/integration/test_render.py @@ -24,10 +24,9 @@ def click_redo(self): html_div(id="b") end -callback!(app, CallbackId( - input = [(:a, :value)], - output = [(:b, :children)] - ) +callback!(app, + Output("b","children"), + Input("a","value") ) do inputValue return inputValue end From 7c36096ed0b7a863cc0dd63bd1e641f107f5f9ea Mon Sep 17 00:00:00 2001 From: Alexandr Romanenko Date: Sun, 7 Jun 2020 03:39:45 +0300 Subject: [PATCH 2/2] flat callbacks --- src/app/callbacks.jl | 73 +++++++++-- src/app/supporttypes.jl | 10 +- src/handler/handlers.jl | 3 +- test/callbacks.jl | 118 +++++++++++++++++- .../jlcbsc006_multiple_outputs.jl | 2 +- .../jlcbsc008_prevent_update_multiple.jl | 2 +- .../jlcbsc009_single_element_array_output.jl | 19 +++ .../callbacks/test_basic_callback.py | 19 +++ .../jlclsd004_clientside_multiple_outputs.jl | 2 +- 9 files changed, 229 insertions(+), 19 deletions(-) create mode 100644 test/integration/callbacks/jl_basic_callback/jlcbsc009_single_element_array_output.jl diff --git a/src/app/callbacks.jl b/src/app/callbacks.jl index 79edff1..ae3e79b 100644 --- a/src/app/callbacks.jl +++ b/src/app/callbacks.jl @@ -1,13 +1,14 @@ dependency_string(dep::Dependency) = "$(dep.id).$(dep.property)" + function output_string(deps::CallbackDeps) - if length(deps.output) == 1 - return dependency_string(deps.output[1]) + if deps.multi_out + return ".." * + join(dependency_string.(deps.output), "...") * + ".." end - return ".." * - join(dependency_string.(deps.output), "...") * - ".." + return dependency_string(deps.output[1]) end @@ -21,7 +22,6 @@ end Create a callback that updates the output by calling function `func`. - # Examples ```julia @@ -37,7 +37,7 @@ end callback!(app, [Output("outputID2", "children"), Output("outputID", "children")], Input("graphTitle", "value"), State("graphTitle", "type") - ) do stateType, inputValue + ) do inputValue, stateType return (stateType * "..." * inputValue, inputValue) end ``` @@ -51,6 +51,64 @@ function callback!(func::Union{Function, ClientsideFunction, String}, return _callback!(func, app, CallbackDeps(output, input, state)) end +""" + function callback!(func::Union{Function, ClientsideFunction, String}, + app::DashApp, + deps... + ) + +Create a callback that updates the output by calling function `func`. +"Flat" version of `callback!` function, `deps` must be ``Output..., Input...[,State...]`` + +# Examples + +```julia +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, deps::CallbackDeps) check_callback(func, app, deps) @@ -80,6 +138,7 @@ end function check_callback(func, app::DashApp, deps::CallbackDeps) + 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(deps.output) != length(unique(deps.output)) && error("One or more callback outputs have been duplicated; please confirm that all outputs are unique.") diff --git a/src/app/supporttypes.jl b/src/app/supporttypes.jl index 61258a3..b02981b 100644 --- a/src/app/supporttypes.jl +++ b/src/app/supporttypes.jl @@ -16,14 +16,16 @@ const IdProp = Tuple{Symbol, Symbol} struct CallbackDeps - output ::Vector{Output} + output ::Vector{Output} input ::Vector{Input} state ::Vector{State} - CallbackDeps(output, input, state = State[]) = new(output, input, state) - CallbackDeps(;output, input, state = State[]) = new(output, input, 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{T}}, v::T) where {T<:Dependency}= [v] struct ClientsideFunction namespace ::String diff --git a/src/handler/handlers.jl b/src/handler/handlers.jl index 97502a6..8f18527 100644 --- a/src/handler/handlers.jl +++ b/src/handler/handlers.jl @@ -54,7 +54,7 @@ function _process_callback(app::DashApp, body::String) end res = app.callbacks[output].func(args...) - if length(app.callbacks[output].dependencies.output) == 1 + if !app.callbacks[output].dependencies.multi_out if !(res isa NoUpdate) return Dict( :response => Dict( @@ -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) diff --git a/test/callbacks.jl b/test/callbacks.jl index 5dcb588..d527914 100644 --- a/test/callbacks.jl +++ b/test/callbacks.jl @@ -4,7 +4,7 @@ using Dash -@testset "callback!" begin +@testset "callback! single output" begin app = dash() app.layout = html_div() do dcc_input(id = "my-id", value="initial value", type = "text"), @@ -31,6 +31,18 @@ using Dash @test :clientside_function in keys(cb) @test isnothing(cb.clientside_function) + handler = Dash.make_handler(app) + test_json = """{"output":"my-div.children","changedPropIds":["my-id.value"],"inputs":[{"id":"my-id","property":"value","value":"test"}]}""" + request = HTTP.Request("POST", "/_dash-update-component", [], Vector{UInt8}(test_json)) + response = HTTP.handle(handler, request) + @test response.status == 200 + resp_obj = JSON2.read(String(response.body)) + @test !in(:multi, keys(resp_obj)) + @test resp_obj.response.props.children == "test" + +end + +@testset "callback! multi output" begin app = dash() app.layout = html_div() do dcc_input(id = "my-id", value="initial value", type = "text"), @@ -42,7 +54,74 @@ using Dash end @test length(app.callbacks) == 1 @test haskey(app.callbacks, Symbol("..my-div.children...my-div2.children..")) - @test app.callbacks[Symbol("..my-div.children...my-div2.children..")].func("state", "value") == ("state", "value") + @test app.callbacks[Symbol("..my-div.children...my-div2.children..")].func("value", "state") == ("value", "state") + + handler = Dash.make_handler(app) + test_json = """{"output":"..my-div.children...my-div2.children..","changedPropIds":["my-id.value"],"inputs":[{"id":"my-id","property":"value","value":"test"}], "state":[{"id":"my-id","property":"type","value":"state"}]}""" + request = HTTP.Request("POST", "/_dash-update-component", [], Vector{UInt8}(test_json)) + response = HTTP.handle(handler, request) + @test response.status == 200 + resp_obj = JSON2.read(String(response.body)) + @test in(:multi, keys(resp_obj)) + @test resp_obj.response[Symbol("my-div")].children == "test" + @test resp_obj.response[Symbol("my-div2")].children == "state" + + app = dash() + app.layout = html_div() do + dcc_input(id = "my-id", value="initial value", type = "text"), + html_div(id = "my-div"), + html_div(id = "my-div2") + end + callback!(app, [Output("my-div","children")], Input("my-id","value"), State("my-id","type")) do value, state + return (value,) + end + @test length(app.callbacks) == 1 + @test haskey(app.callbacks, Symbol("..my-div.children..")) + @test app.callbacks[Symbol("..my-div.children..")].func("value", "state") == ("value", ) + + handler = Dash.make_handler(app) + test_json = """{"output":"..my-div.children..","changedPropIds":["my-id.value"],"inputs":[{"id":"my-id","property":"value","value":"test"}], "state":[{"id":"my-id","property":"type","value":"state"}]}""" + request = HTTP.Request("POST", "/_dash-update-component", [], Vector{UInt8}(test_json)) + response = HTTP.handle(handler, request) + @test response.status == 200 + resp_obj = JSON2.read(String(response.body)) + @test in(:multi, keys(resp_obj)) + @test resp_obj.response[Symbol("my-div")].children == "test" + +end +@testset "callback! multi output flat" begin + app = dash() + app.layout = html_div() do + dcc_input(id = "my-id", value="initial value", type = "text"), + dcc_input(id = "my-id2", value="initial value", type = "text"), + html_div(id = "my-div"), + html_div(id = "my-div2") + end + callback!(app, Output("my-div","children"), Output("my-div2","children"), + Input("my-id","value"), Input("my-id", "value"), State("my-id","type")) do value, value2, state + return value * value2, state + end + @test length(app.callbacks) == 1 + @test haskey(app.callbacks, Symbol("..my-div.children...my-div2.children..")) + @test app.callbacks[Symbol("..my-div.children...my-div2.children..")].func("value", " value2", "state") == ("value value2", "state") + + app = dash() + app.layout = html_div() do + dcc_input(id = "my-id", value="initial value", type = "text"), + dcc_input(id = "my-id2", value="initial value", type = "text"), + html_div(id = "my-div"), + html_div(id = "my-div2") + end + callback!(app, Output("my-div","children"), + Input("my-id","value"), Input("my-id", "value")) do value, value2 + return value * value2 + end + @test length(app.callbacks) == 1 + @test haskey(app.callbacks, Symbol("my-div.children")) + @test app.callbacks[Symbol("my-div.children")].func("value", " value2") == "value value2" + +end +@testset "callback! checks" begin app = dash() @@ -107,7 +186,40 @@ using Dash return "v_$(value)" end - + app = dash() + + app.layout = html_div() do + dcc_input(id = "my-id", value="initial value", type = "text"), + dcc_input(id = "my-id2", value="initial value", type = "text"), + html_div(id = "my-div"), + html_div(id = "my-div2") + end + #empty output + @test_throws ErrorException callback!(app, + Input("my-id","value")) do value + return "v_$(value)" + end + #empty input + @test_throws ErrorException callback!(app, + Output("my-div2", "children")) do value + return "v_$(value)" + end + + #wrong args order + @test_throws ErrorException callback!(app, + Input("my-id","value"), Output("my-div", "children")) do value + return "v_$(value)" + end + + @test_throws ErrorException callback!(app, + Output("my-div2", "children"), Input("my-id","value"), Output("my-div", "children")) do value + return "v_$(value)" + end + + @test_throws ErrorException callback!(app, + Output("my-div2", "children"), Input("my-id","value"), State("my-div", "children"), Input("my-id2", "value")) do value, value2 + return "v_$(value)" + end end diff --git a/test/integration/callbacks/jl_basic_callback/jlcbsc006_multiple_outputs.jl b/test/integration/callbacks/jl_basic_callback/jlcbsc006_multiple_outputs.jl index a97302c..edf71d1 100644 --- a/test/integration/callbacks/jl_basic_callback/jlcbsc006_multiple_outputs.jl +++ b/test/integration/callbacks/jl_basic_callback/jlcbsc006_multiple_outputs.jl @@ -9,7 +9,7 @@ app.layout = html_div() do html_div(id="output1"), html_div(id="output2") end -callback!(app, [Output("output1","children"), Output("output2","children")], Input("input","value")) do input +callback!(app, Output("output1","children"), Output("output2","children"), Input("input","value")) do input return ("$input first", "$input second") end diff --git a/test/integration/callbacks/jl_basic_callback/jlcbsc008_prevent_update_multiple.jl b/test/integration/callbacks/jl_basic_callback/jlcbsc008_prevent_update_multiple.jl index f816189..616c9f5 100644 --- a/test/integration/callbacks/jl_basic_callback/jlcbsc008_prevent_update_multiple.jl +++ b/test/integration/callbacks/jl_basic_callback/jlcbsc008_prevent_update_multiple.jl @@ -22,7 +22,7 @@ callback!(app, Output("regular_output","children"), Input("input","value")) do i return input end -callback!(app, [Output("output1","children"), Output("output2","children")], Input("input","value")) do input +callback!(app, Output("output1","children"), Output("output2","children"), Input("input","value")) do input input == "prevent" && throw(PreventUpdate()) if input == "no_update1" return (no_update(), input) diff --git a/test/integration/callbacks/jl_basic_callback/jlcbsc009_single_element_array_output.jl b/test/integration/callbacks/jl_basic_callback/jlcbsc009_single_element_array_output.jl new file mode 100644 index 0000000..2f82606 --- /dev/null +++ b/test/integration/callbacks/jl_basic_callback/jlcbsc009_single_element_array_output.jl @@ -0,0 +1,19 @@ + +using Dash +using DashHtmlComponents +using DashCoreComponents + +app = dash() + +app.layout = html_div() do + dcc_input(id="input", value = "initial value"), + html_div(id = "output") +end +callback!(app, + [Output("output","children")], + Input("input","value") + ) do value + return (value,) +end + +run_server(app) diff --git a/test/integration/callbacks/test_basic_callback.py b/test/integration/callbacks/test_basic_callback.py index 883b1d0..8de502f 100644 --- a/test/integration/callbacks/test_basic_callback.py +++ b/test/integration/callbacks/test_basic_callback.py @@ -210,4 +210,23 @@ def test_jlcbsc008_prevent_update(dashjl): ) dashjl.wait_for_text_to_equal( "#output2", "no_update1", timeout=2 + ) + +def test_jlcbsc009_single_element_array_output(dashjl): + fp = jl_test_file_path("jlcbsc009_single_element_array_output.jl") + dashjl.start_server(fp) + + dashjl.wait_for_element_by_css_selector( + "#input", timeout=2 + ) + + dashjl.wait_for_text_to_equal( + "#output", "initial value", timeout=2 + ) + input_ = dashjl.find_element("#input") + dashjl.clear_input(input_) + input_.send_keys("hello world") + + dashjl.wait_for_text_to_equal( + "#output", "hello world", timeout=1 ) \ No newline at end of file diff --git a/test/integration/clientside/jl_clientside/jlclsd004_clientside_multiple_outputs.jl b/test/integration/clientside/jl_clientside/jlclsd004_clientside_multiple_outputs.jl index 633477a..29aef84 100644 --- a/test/integration/clientside/jl_clientside/jlclsd004_clientside_multiple_outputs.jl +++ b/test/integration/clientside/jl_clientside/jlclsd004_clientside_multiple_outputs.jl @@ -15,7 +15,7 @@ end callback!( ClientsideFunction("clientside", "add_to_four_outputs"), app, - [Output("output-1","value"), Output("output-2","value"), Output("output-3","value"), Output("output-4","value")], + Output("output-1","value"), Output("output-2","value"), Output("output-3","value"), Output("output-4","value"), Input("input","value") )