Skip to content

Commit 9d2d74f

Browse files
authored
Merge pull request #30 from plotly/clientside_callbacks
Clientside callbacks
2 parents 645d218 + eb54990 commit 9d2d74f

21 files changed

+682
-146
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file. This projec
55

66
### [UNRELEASED]
77
### Added
8+
- Support for client side callbacks [#30](https://github.com/plotly/Dash.jl/pull/30)
89
- Support for hot reloading on application or asset changes [#25](https://github.com/plotly/Dash.jl/pull/25)
910
- Asset serving of CSS, JavaScript, and other resources [#18](https://github.com/plotly/Dash.jl/pull/18)
1011
- Support for passing functions as layouts [#18](https://github.com/plotly/Dash.jl/pull/18)

src/Dash.jl

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import .Front
99
using .Components
1010

1111
export dash, Component, Front, <|, @callid_str, CallbackId, callback!,
12-
enable_dev_tools!,
12+
enable_dev_tools!, ClientsideFunction,
1313
run_server, PreventUpdate, no_update, @var_str
1414

1515

src/app/callbacks.jl

+26-5
Original file line numberDiff line numberDiff line change
@@ -49,17 +49,32 @@ end
4949
5050
5151
"""
52-
function callback!(func::Function, app::DashApp, id::CallbackId)
52+
function callback!(func::Union{Function, ClientsideFunction, String}, app::DashApp, id::CallbackId)
5353

5454
check_callback(func, app, id)
5555

5656
out_symbol = Symbol(output_string(id))
57-
58-
push!(app.callbacks, out_symbol => Callback(func, id))
57+
callback_func = make_callback_func!(app, func, id)
58+
push!(app.callbacks, out_symbol => Callback(callback_func, id))
5959
end
6060

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

62-
function check_callback(func::Function, app::DashApp, id::CallbackId)
63+
function make_callback_func!(app::DashApp, func::String, id::CallbackId)
64+
first_output = first(id.output)
65+
namespace = replace("_dashprivate_$(first_output[1])", "\""=>"\\\"")
66+
function_name = replace("$(first_output[2])", "\""=>"\\\"")
67+
68+
function_string = """
69+
var clientside = window.dash_clientside = window.dash_clientside || {};
70+
var ns = clientside["$namespace"] = clientside["$namespace"] || {};
71+
ns["$function_name"] = $func;
72+
"""
73+
push!(app.inline_scripts, function_string)
74+
return ClientsideFunction(namespace, function_name)
75+
end
76+
77+
function check_callback(func, app::DashApp, id::CallbackId)
6378

6479

6580

@@ -75,9 +90,15 @@ function check_callback(func::Function, app::DashApp, id::CallbackId)
7590

7691
args_count = length(id.state) + length(id.input)
7792

78-
!hasmethod(func, NTuple{args_count, Any}) && error("The arguments of the specified callback function do not align with the currently defined callback; please ensure that the arguments to `func` are properly defined.")
93+
check_callback_func(func, args_count)
7994

8095
for id_prop in id.input
8196
id_prop in id.output && error("Circular input and output arguments were found. Please verify that callback outputs are not also input arguments.")
8297
end
8398
end
99+
100+
function check_callback_func(func::Function, args_count)
101+
!hasmethod(func, NTuple{args_count, Any}) && error("The arguments of the specified callback function do not align with the currently defined callback; please ensure that the arguments to `func` are properly defined.")
102+
end
103+
function check_callback_func(func, args_count)
104+
end

src/app/dashapp.jl

+3-1
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,10 @@ mutable struct DashApp
3333
layout ::Union{Nothing, Component, Function}
3434
devtools ::DevTools
3535
callbacks ::Dict{Symbol, Callback}
36+
inline_scripts ::Vector{String}
3637

37-
DashApp(root_path, is_interactive, config, index_string, title = "Dash") = new(root_path, is_interactive, config, index_string, title, nothing, DevTools(dash_env(Bool, "debug", false)), Dict{Symbol, Callback}())
38+
DashApp(root_path, is_interactive, config, index_string, title = "Dash") =
39+
new(root_path, is_interactive, config, index_string, title, nothing, DevTools(dash_env(Bool, "debug", false)), Dict{Symbol, Callback}(), String[])
3840

3941
end
4042

src/app/supporttypes.jl

+5-2
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,12 @@ CallbackId(;input,
1313

1414

1515
Base.convert(::Type{Vector{IdProp}}, v::IdProp) = [v]
16-
16+
struct ClientsideFunction
17+
namespace ::String
18+
function_name ::String
19+
end
1720
struct Callback
18-
func ::Function
21+
func ::Union{Function, ClientsideFunction}
1922
id ::CallbackId
2023
end
2124

src/handler/index_page.jl

+9-3
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ make_css_tag(app::DashApp, resource::AppResource) = make_css_tag(resource_url(ap
3434
make_script_tag(url::String) = """<script src="$(url)"></script>"""
3535
make_script_tag(app::DashApp, resource::AppCustomResource) = format_tag("script", resource.params)
3636
make_script_tag(app::DashApp, resource::AppResource) = make_script_tag(resource_url(app, resource))
37+
make_inline_script_tag(script::String) = """<script>$(script)</script>"""
3738

3839
function metas_html(app::DashApp)
3940
meta_tags = app.config.meta_tags
@@ -58,9 +59,14 @@ function css_html(app::DashApp, resources::ApplicationResources)
5859
end
5960

6061
function scripts_html(app::DashApp, resources::ApplicationResources)
61-
join(
62-
make_script_tag.(Ref(app), resources.js), "\n "
63-
)
62+
include_string = join(
63+
vcat(
64+
make_script_tag.(Ref(app), resources.js),
65+
make_inline_script_tag.(app.inline_scripts)
66+
),
67+
"\n "
68+
)
69+
6470
end
6571

6672
app_entry_html() = """

src/handler/state.jl

+4-1
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,15 @@ mutable struct StateCache
1919
StateCache(app, registry) = new(_cache_tuple(app, registry)..., false)
2020
end
2121

22+
_dep_clientside_func(func::ClientsideFunction) = func
23+
_dep_clientside_func(func) = nothing
2224
function _dependencies_json(app::DashApp)
2325
id_prop_named(p::IdProp) = (id = p[1], property = p[2])
2426
result = map(values(app.callbacks)) do dep
2527
(inputs = id_prop_named.(dep.id.input),
2628
state = id_prop_named.(dep.id.state),
27-
output = output_string(dep.id)
29+
output = output_string(dep.id),
30+
clientside_function = _dep_clientside_func(dep.func)
2831
)
2932
end
3033
return JSON2.write(result)

test/callbacks.jl

+208
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
import HTTP, JSON2
2+
using Test
3+
using Dash
4+
5+
@testset "callid" begin
6+
id = callid"id1.prop1 => id2.prop2"
7+
@test id isa CallbackId
8+
@test length(id.state) == 0
9+
@test length(id.input) == 1
10+
@test length(id.output) == 1
11+
@test id.input[1] == (:id1, :prop1)
12+
@test id.output[1] == (:id2, :prop2)
13+
14+
id = callid"{state1.prop1, state2.prop2} input1.prop1, input2.prop2 => output1.prop1, output2.prop2"
15+
@test id isa CallbackId
16+
@test length(id.state) == 2
17+
@test length(id.input) == 2
18+
@test length(id.output) == 2
19+
@test id.input[1] == (:input1, :prop1)
20+
@test id.input[2] == (:input2, :prop2)
21+
@test id.output[1] == (:output1, :prop1)
22+
@test id.output[2] == (:output2, :prop2)
23+
@test id.state[1] == (:state1, :prop1)
24+
@test id.state[2] == (:state2, :prop2)
25+
end
26+
27+
@testset "callback!" begin
28+
app = dash()
29+
app.layout = html_div() do
30+
dcc_input(id = "my-id", value="initial value", type = "text"),
31+
html_div(id = "my-div")
32+
end
33+
34+
callback!(app, callid"my-id.value => my-div.children") do value
35+
return value
36+
end
37+
@test length(app.callbacks) == 1
38+
@test haskey(app.callbacks, Symbol("my-div.children"))
39+
@test app.callbacks[Symbol("my-div.children")].func("test") == "test"
40+
41+
handler = make_handler(app)
42+
request = HTTP.Request("GET", "/_dash-dependencies")
43+
resp = HTTP.handle(handler, request)
44+
deps = JSON2.read(String(resp.body))
45+
46+
@test length(deps) == 1
47+
cb = deps[1]
48+
@test cb.output == "my-div.children"
49+
@test cb.inputs[1].id == "my-id"
50+
@test cb.inputs[1].property == "value"
51+
@test :clientside_function in keys(cb)
52+
@test isnothing(cb.clientside_function)
53+
54+
app = dash()
55+
app.layout = html_div() do
56+
dcc_input(id = "my-id", value="initial value", type = "text"),
57+
html_div(id = "my-div"),
58+
html_div(id = "my-div2")
59+
end
60+
callback!(app, callid"{my-id.type} my-id.value => my-div.children, my-div2.children") do state, value
61+
return state, value
62+
end
63+
@test length(app.callbacks) == 1
64+
@test haskey(app.callbacks, Symbol("..my-div.children...my-div2.children.."))
65+
@test app.callbacks[Symbol("..my-div.children...my-div2.children..")].func("state", "value") == ("state", "value")
66+
67+
app = dash()
68+
69+
app.layout = html_div() do
70+
dcc_input(id = "my-id", value="initial value", type = "text"),
71+
html_div(id = "my-div"),
72+
html_div(id = "my-div2")
73+
end
74+
75+
callback!(app, callid"my-id.value => my-div.children") do value
76+
return value
77+
end
78+
callback!(app, callid"my-id.value => my-div2.children") do value
79+
return "v_$(value)"
80+
end
81+
82+
@test length(app.callbacks) == 2
83+
@test haskey(app.callbacks, Symbol("my-div.children"))
84+
@test haskey(app.callbacks, Symbol("my-div2.children"))
85+
@test app.callbacks[Symbol("my-div.children")].func("value") == "value"
86+
@test app.callbacks[Symbol("my-div2.children")].func("value") == "v_value"
87+
88+
@test_throws ErrorException callback!(app, callid"my-id.value => my-div2.children") do value
89+
return "v_$(value)"
90+
end
91+
92+
@test_throws ErrorException callback!(app, callid"{my-id.value} my-id.value => my-id.value") do value
93+
return "v_$(value)"
94+
end
95+
96+
@test_throws ErrorException callback!(app, callid"my-div.children, my-id.value => my-id.value") do value
97+
return "v_$(value)"
98+
end
99+
@test_throws ErrorException callback!(app, callid"my-id.value => my-div.children, my-id.value") do value
100+
return "v_$(value)"
101+
end
102+
103+
@test_throws ErrorException callback!(app, callid" => my-div2.title, my-id.value") do value
104+
return "v_$(value)"
105+
end
106+
107+
@test_throws ErrorException callback!(app, callid"my-id.value => my-div2.title, my-div2.title") do value
108+
return "v_$(value)"
109+
end
110+
111+
@test_throws ErrorException callback!(app, callid"my-id.value => my-div2.title") do
112+
return "v_"
113+
end
114+
115+
116+
app = dash()
117+
app.layout = html_div() do
118+
dcc_input(id = "my-id", value="initial value", type = "text"),
119+
html_div("test2", id = "my-div"),
120+
html_div(id = "my-div2") do
121+
html_h1("gggg", id = "my-h")
122+
end
123+
end
124+
callback!(app, callid"{my-id.type} my-id.value => my-div.children, my-h.children") do state, value
125+
return state, value
126+
end
127+
@test length(app.callbacks) == 1
128+
end
129+
130+
@testset "clientside callbacks function" begin
131+
app = dash()
132+
app.layout = html_div() do
133+
dcc_input(id = "my-id", value="initial value", type = "text"),
134+
html_div(id = "my-div")
135+
end
136+
137+
callback!(ClientsideFunction("namespace", "func_name"),app, callid"my-id.value => my-div.children")
138+
139+
@test length(app.callbacks) == 1
140+
@test haskey(app.callbacks, Symbol("my-div.children"))
141+
@test app.callbacks[Symbol("my-div.children")].func isa ClientsideFunction
142+
@test app.callbacks[Symbol("my-div.children")].func.namespace == "namespace"
143+
@test app.callbacks[Symbol("my-div.children")].func.function_name == "func_name"
144+
145+
@test_throws ErrorException callback!(app, callid"my-id.value => my-div.children") do value
146+
return "v_$(value)"
147+
end
148+
149+
handler = make_handler(app)
150+
request = HTTP.Request("GET", "/_dash-dependencies")
151+
resp = HTTP.handle(handler, request)
152+
deps = JSON2.read(String(resp.body))
153+
154+
@test length(deps) == 1
155+
cb = deps[1]
156+
@test cb.output == "my-div.children"
157+
@test cb.inputs[1].id == "my-id"
158+
@test cb.inputs[1].property == "value"
159+
@test :clientside_function in keys(cb)
160+
@test cb.clientside_function.namespace == "namespace"
161+
@test cb.clientside_function.function_name == "func_name"
162+
end
163+
@testset "clientside callbacks string" begin
164+
app = dash()
165+
app.layout = html_div() do
166+
dcc_input(id = "my-id", value="initial value", type = "text"),
167+
html_div(id = "my-div")
168+
end
169+
170+
callback!(
171+
"""
172+
function(input_value) {
173+
return (
174+
parseFloat(input_value_1, 10)
175+
);
176+
}
177+
"""
178+
, app, callid"my-id.value => my-div.children"
179+
)
180+
181+
@test length(app.callbacks) == 1
182+
@test haskey(app.callbacks, Symbol("my-div.children"))
183+
@test app.callbacks[Symbol("my-div.children")].func isa ClientsideFunction
184+
@test app.callbacks[Symbol("my-div.children")].func.namespace == "_dashprivate_my-div"
185+
@test app.callbacks[Symbol("my-div.children")].func.function_name == "children"
186+
@test length(app.inline_scripts) == 1
187+
@test occursin("clientside[\"_dashprivate_my-div\"]", app.inline_scripts[1])
188+
@test occursin("ns[\"children\"]", app.inline_scripts[1])
189+
190+
handler = make_handler(app)
191+
request = HTTP.Request("GET", "/_dash-dependencies")
192+
resp = HTTP.handle(handler, request)
193+
deps = JSON2.read(String(resp.body))
194+
195+
@test length(deps) == 1
196+
cb = deps[1]
197+
@test cb.output == "my-div.children"
198+
@test cb.inputs[1].id == "my-id"
199+
@test cb.inputs[1].property == "value"
200+
@test :clientside_function in keys(cb)
201+
@test cb.clientside_function.namespace == "_dashprivate_my-div"
202+
@test cb.clientside_function.function_name == "children"
203+
request = HTTP.Request("GET", "/")
204+
resp = HTTP.handle(handler, request)
205+
body = String(resp.body)
206+
@test occursin("clientside[\"_dashprivate_my-div\"]", body)
207+
@test occursin("ns[\"children\"]", body)
208+
end

0 commit comments

Comments
 (0)