diff --git a/src/Elements.jl b/src/Elements.jl index 7cea46ca..f2a7742b 100644 --- a/src/Elements.jl +++ b/src/Elements.jl @@ -15,7 +15,7 @@ export root, elem, vm, @if, @else, @elseif, @for, @text, @bind, @data, @on, @cli # deprecated exports export @iif, @els, @elsiif, @recur -export @jsexpr, JSExpr, js_quote_replace, ∥, ∧ +export @jsexpr, JSExpr, js_quote_replace, ∥, ∧, jse_str export stylesheet, kw_to_str export add_plugins, remove_plugins @@ -475,6 +475,10 @@ macro jsexpr(expr) :(JSExpr($ex)) end +macro jse_str(expr) + JSExpr(expr) +end + """ @if(expr) diff --git a/src/ReactiveTools.jl b/src/ReactiveTools.jl index 6048a983..46b00cce 100644 --- a/src/ReactiveTools.jl +++ b/src/ReactiveTools.jl @@ -1346,12 +1346,12 @@ function vue_options(hook_type, args...) expr = args[1] quote let M = Stipple.@type - Stipple.$(Symbol("js_$hook_type"))(::M) = $expr + Stipple.$(Symbol("js_$hook_type"))(::Type{<:M}) = $expr end end |> esc elseif length(args) == 2 T, expr = args[1], args[2] - esc(:(Stipple.$(Symbol("js_$hook_type"))(::$T) = $expr)) + esc(:(Stipple.$(Symbol("js_$hook_type"))(::Type{<:$T}) = $expr)) else error("Invalid number of arguments for vue options") end @@ -1464,7 +1464,7 @@ macro client_data(expr) esc(quote let M = Stipple.@type - Stipple.client_data(::M) = $output + Stipple.client_data(::Type{<:M}) = $output end end) end @@ -1481,7 +1481,7 @@ macro client_data(M, expr) push!(output.args, e) end - :(Stipple.client_data(::$(esc(M))) = $(esc(output))) + :(Stipple.client_data(::Type{<:$(esc(M))}) = $(esc(output))) end macro add_client_data(expr) @@ -1500,7 +1500,7 @@ macro add_client_data(expr) let M = Stipple.@type cd_old = Stipple.client_data(M()) cd_new = $output - Stipple.client_data(::M) = merge(d1, d2) + Stipple.client_data(::Type{<:M}) = merge(d1, d2) end end) end diff --git a/src/stipple/jsmethods.jl b/src/stipple/jsmethods.jl index 2ead91ab..41a5d4c2 100644 --- a/src/stipple/jsmethods.jl +++ b/src/stipple/jsmethods.jl @@ -1,5 +1,5 @@ """ - function js_methods(app::T) where {T<:ReactiveModel} + function js_methods(::Type{<:T}) where {T<:ReactiveModel} Defines js functions for the `methods` section of the vue element. Expected result types of the function are @@ -12,7 +12,7 @@ Expected result types of the function are ### Example 1 ```julia -js_methods(::MyDashboard) = \"\"\" +js_methods(::Type{<:MyDashboard}) = \"\"\" mysquare: function (x) { return x^2 } @@ -23,18 +23,17 @@ js_methods(::MyDashboard) = \"\"\" ``` ### Example 2 ``` -js_methods(::MyDashboard) = Dict(:f => "function(x) { console.log('x: ' + x) }) +js_methods(::Type{<:MyDashboard}) = Dict(:f => "function(x) { console.log('x: ' + x) }) ``` ### Example 3 ``` js_greet() = :greet => "function(name) {console.log('Hello ' + name)}" js_bye() = :bye => "function() {console.log('Bye!')}" -js_methods(::MyDashboard) = [js_greet, js_bye] +js_methods(::Type{<:MyDashboard}) = [js_greet, js_bye] ``` """ -function js_methods(app::T)::String where {T<:ReactiveModel} - "" -end +js_methods(::DataType) = "" +js_methods(::T) where T = js_methods(T) # deprecated, now part of the model function js_methods_events()::String @@ -51,7 +50,7 @@ function js_methods_events()::String end """ - function js_computed(app::T) where {T<:ReactiveModel} + function js_computed(::Type{<:T}) where {T<:ReactiveModel} Defines js functions for the `computed` section of the vue element. These properties are updated every time on of the inner parameters changes its value. @@ -72,14 +71,13 @@ js_computed(app::MyDashboard) = \"\"\" \"\"\" ``` """ -function js_computed(app::T)::String where {T<:ReactiveModel} - "" -end +js_computed(::DataType) = "" +js_computed(::T) where T = js_computed(T) const jscomputed = js_computed """ - function js_watch(app::T) where {T<:ReactiveModel} + function js_watch(::Type{<:T}) where {T<:ReactiveModel} Defines js functions for the `watch` section of the vue element. These functions are called every time the respective property changes. @@ -95,7 +93,7 @@ Expected result types of the function are Updates the `fullName` every time `firstName` or `lastName` changes. ```julia -js_watch(app::MyDashboard) = \"\"\" +js_watch(::Type{<:MyDashboard}) = \"\"\" firstName: function (val) { this.fullName = val + ' ' + this.lastName }, @@ -105,14 +103,13 @@ js_watch(app::MyDashboard) = \"\"\" \"\"\" ``` """ -function js_watch(m::T)::String where {T<:ReactiveModel} - "" -end +js_watch(::DataType) = "" +js_watch(::T) where T = js_watch(T) const jswatch = js_watch """ - function client_data(app::T)::String where {T<:ReactiveModel} + function client_data(::Type{<:MyApp})::String Defines additional data that will only be visible by the browser. @@ -122,13 +119,21 @@ In order to use the data you most probably also want to define [`js_methods`](@r ```julia import Stipple.client_data -client_data(m::Example) = client_data(client_name = js"null", client_age = js"null", accept = false) +client_data(::Type{<:Example}) = client_data(client_name = js"null", client_age = js"null", accept = false) +``` +will define the additional fields `client_name`, `client_age` and `accept` for models of type `Example`. +These definitions should, of course, not overlap with existing fields of your model. +### Note +Previously we defined the client_data function differently. This will continue to work, +but the new way might have some advantages in the future for mixins. Here is the old way: +```julia +client_data(::Example) = client_data(client_name = js"null", client_age = js"null", accept = false) ``` -will define the additional fields `client_name`, `client_age` and `accept` for the model `Example`. These should, of course, not overlap with existing fields of your model. """ -client_data(app::T) where T <: ReactiveModel = Dict{String, Any}() +client_data(::Type{<:ReactiveModel}) = Dict{String, Any}() +client_data(::T) where T = client_data(T) -client_data(;kwargs...) = Dict{String, Any}([String(k) => v for (k, v) in kwargs]...) +client_data(; kwargs...) = Dict{String, Any}([String(k) => v for (k, v) in kwargs]...) for (f, field) in ( (:js_before_create, :beforeCreate), (:js_created, :created), (:js_before_mount, :beforeMount), (:js_mounted, :mounted), @@ -138,7 +143,7 @@ for (f, field) in ( field_str = string(field) Core.eval(@__MODULE__, quote """ - function $($f)(app::T)::Union{Function, String, Vector} where {T<:ReactiveModel} + function $($f)(::Type{<:T})::Union{Function, String, Vector} where {T<:ReactiveModel} Defines js statements for the `$($field_str)` section of the vue element. @@ -150,7 +155,7 @@ for (f, field) in ( ### Example 1 ```julia - $($f)(app::MyDashboard) = \"\"\" + $($f)(::Type{<:MyDashboard}) = \"\"\" if (this.cameraon) { startcamera() } \"\"\" ``` @@ -161,17 +166,22 @@ for (f, field) in ( startcamera() = "if (this.cameraon) { startcamera() }" stopcamera() = "if (this.cameraon) { stopcamera() }" - $($f)(app::MyDashboard) = [startcamera, stopcamera] + $($f)(::Type{<:MyDashboard}) = [startcamera, stopcamera] ``` Checking the result can be done in the following way ``` julia> render(MyApp())[:$($field_str)] JSONText("function(){\n if (this.cameraon) { startcamera() }\n\n if (this.cameraon) { stopcamera() }\n}") ``` + ### Note + Previously we defined the function differently. This will continue to work, + but the new way might have some advantages in the future for mixins. Here is the old way: + ```julia + $($f)(::MyDashboard) = [startcamera, stopcamera] + ``` """ - function $f(app::T)::String where {T<:ReactiveModel} - "" - end + $f(::DataType)::String = "" + $f(::T) where T = $f(T) end) end @@ -253,13 +263,11 @@ function js_initscript(initscript::String) """ end -function js_created_auto(x) -"" -end +js_created_auto(::DataType) = "" +js_created_auto(::T) where T = js_created_auto(T) -function js_watch_auto(x) -"" -end +js_watch_auto(::DataType) = "" +js_watch_auto(::T) where T = js_watch_auto(T) # methods to be used directly as arguments to js_methods diff --git a/src/stipple/reactivity.jl b/src/stipple/reactivity.jl index 5d3d1ac0..accba9b1 100644 --- a/src/stipple/reactivity.jl +++ b/src/stipple/reactivity.jl @@ -155,8 +155,8 @@ end """ abstract type ReactiveModel end -struct Mixin - mixin::Union{Expr, Symbol, QuoteNode} +struct MixinExpr + M::Union{Expr, Symbol, QuoteNode} prefix::String postfix::String end @@ -437,7 +437,7 @@ function parse_mixin_params(params) end macro mixin(expr...) - Mixin(parse_mixin_params(collect(expr))...) + MixinExpr(parse_mixin_params(collect(expr))...) end function add_brackets!(expr, varnames) @@ -475,70 +475,73 @@ macro var_storage(expr, handler = nothing) let_block = Expr(:block, :(_ = 0)) required_evals!.(expr.args, RefValue(required_vars), RefValue(all_vars)) add_brackets!.(expr.args, RefValue(required_vars)) + mixins = Mixin[] for e in expr.args - if e isa LineNumberNode - source = e - continue - end - mode = :PUBLIC - reactive = true - if e isa Expr && e.head == :(=) - #check whether flags are set - if e.args[end] isa Expr && e.args[end].head == :tuple - flags = e.args[end].args[2:end] - if length(flags) > 0 && flags[1] ∈ [:PUBLIC, :READONLY, :PRIVATE, :JSFUNCTION, :NON_REACTIVE] - newmode = intersect(setdiff(flags, [:NON_REACTIVE]), [:READONLY, :PRIVATE, :JSFUNCTION]) - length(newmode) > 0 && (mode = newmode[end]) - reactive = :NON_REACTIVE ∉ flags - e.args[end] = e.args[end].args[1] - end - end - var, ex = parse_expression!(e, reactive ? mode : nothing, source, m, let_block, required_vars) - # prevent overwriting of control fields - var ∈ [INTERNALFIELDS..., AUTOFIELDS...] && continue - if reactive == false - Stipple.setmode!(storage[:modes__], Core.eval(Stipple, mode), var) + if e isa LineNumberNode + source = e + continue + end + mode = :PUBLIC + reactive = true + if e isa Expr && e.head == :(=) + #check whether flags are set + if e.args[end] isa Expr && e.args[end].head == :tuple + flags = e.args[end].args[2:end] + if length(flags) > 0 && flags[1] ∈ [:PUBLIC, :READONLY, :PRIVATE, :JSFUNCTION, :NON_REACTIVE] + newmode = intersect(setdiff(flags, [:NON_REACTIVE]), [:READONLY, :PRIVATE, :JSFUNCTION]) + length(newmode) > 0 && (mode = newmode[end]) + reactive = :NON_REACTIVE ∉ flags + e.args[end] = e.args[end].args[1] end + end + var, ex = parse_expression!(e, reactive ? mode : nothing, source, m, let_block, required_vars) + # prevent overwriting of control fields + var ∈ [INTERNALFIELDS..., AUTOFIELDS...] && continue + if reactive == false + Stipple.setmode!(storage[:modes__], Core.eval(Stipple, mode), var) + end - storage[var] = ex - else - if e isa Mixin - mixin, prefix, postfix = e.mixin, e.prefix, e.postfix - mixin_storage = Stipple.var_to_storage(@eval(m, $mixin), prefix, postfix; mixin_name = mixin) - - pre_length = lastindex(prefix) - post_length = lastindex(postfix) - - handlers_expr_name = Symbol(mixin, :var"!_handlers_expr") - handlers_expr = if pre_length + post_length > 0 && isdefined(m, handlers_expr_name) - varnames = setdiff(collect(keys(mixin_storage)), Stipple.AUTOFIELDS, Stipple.INTERNALFIELDS) - oldvarnames = [Symbol("$var"[1 + pre_length:end-post_length]) for var in varnames] - # make a deepcopy of the handlers_expr, because we modify it by prefix and postfix - handlers_expr = deepcopy(@eval(m, $handlers_expr_name)) - for h in handlers_expr - h isa Expr || continue - postwalk!(h) do x - if x isa Symbol && x ∈ oldvarnames - Symbol(prefix, x, postfix) - elseif x isa QuoteNode && x.value isa Symbol && x.value ∈ oldvarnames - QuoteNode(Symbol(prefix, x.value, postfix)) - else - x - end + storage[var] = ex + else + if e isa MixinExpr + M_expr, prefix, postfix = e.M, e.prefix, e.postfix + M = @eval(m, $M_expr) + push!(mixins, Mixin(M, prefix, postfix)) + mixin_storage = Stipple.var_to_storage(M, prefix, postfix; mixin_name = M_expr) + + pre_length = lastindex(prefix) + post_length = lastindex(postfix) + + handlers_expr_name = Symbol(M_expr, :var"!_handlers_expr") + handlers_expr = if pre_length + post_length > 0 && isdefined(m, handlers_expr_name) + varnames = setdiff(collect(keys(mixin_storage)), Stipple.AUTOFIELDS, Stipple.INTERNALFIELDS) + oldvarnames = [Symbol("$var"[1 + pre_length:end-post_length]) for var in varnames] + # make a deepcopy of the handlers_expr, because we modify it by prefix and postfix + handlers_expr = deepcopy(@eval(m, $handlers_expr_name)) + for h in handlers_expr + h isa Expr || continue + postwalk!(h) do x + if x isa Symbol && x ∈ oldvarnames + Symbol(prefix, x, postfix) + elseif x isa QuoteNode && x.value isa Symbol && x.value ∈ oldvarnames + QuoteNode(Symbol(prefix, x.value, postfix)) + else + x end end - vcat([prefix, postfix], handlers_expr) - else - nothing end - merge!(storage, merge_storage(storage, mixin_storage; context = m, handlers_expr)) + vcat([prefix, postfix], handlers_expr) + else + nothing end - :modes__, e + merge!(storage, merge_storage(storage, mixin_storage; context = m, handlers_expr)) end - + :modes__, e end + end - esc(:($storage)) + storage[:mixins__] = Expr(:ref, Stipple.Mixin, mixins...) + esc(:($storage)) end Stipple.Genie.Router.delete!(M::Type{<:ReactiveModel}) = Stipple.Genie.Router.delete!(Symbol(Stipple.routename(M))) @@ -599,14 +602,26 @@ function restore_constructor(::Type{T}) where T<:ReactiveModel end macro type(modelname, storage) - modelname isa DataType && (modelname = modelname.name.name) + M = if modelname isa DataType + modelname = modelname.name.name + parentmodule(modelname) + else + __module__ + end + modelconst = Symbol(modelname, '!') output = quote end - output.args = @eval __module__ collect(values($storage)) - output_qn = QuoteNode(output) + storage = @eval __module__ $storage - quote + mixins = if haskey(storage, :mixins__) + __module__.eval(pop!(storage, :mixins__)) + else + Mixin[] + end + output.args = collect(values(storage)) + + ex = quote abstract type $modelname <: Stipple.ReactiveModel end Stipple.@kwredef mutable struct $modelconst <: $modelname @@ -634,7 +649,16 @@ macro type(modelname, storage) Stipple.Genie.Router.delete!(Symbol(Stipple.routename($modelname))) $modelname - end |> esc + end + + if isempty(mixins) + if isdefined(M, modelname) + insert!(ex.args, 3, :(Base.delete_method.(methods(Stipple.mixins, (Type{<:$modelname},), $M)))) + end + else + insert!(ex.args, 3, :(Stipple.mixins(::Type{<:$modelname}) = $mixins)) + end + ex |> esc end """ @@ -651,13 +675,14 @@ end """ macro vars(modelname, expr) quote - Stipple.@type($modelname, values(Stipple.@var_storage($expr))) + Stipple.@type($modelname, Stipple.@var_storage($expr)) end |> esc end macro define_mixin(mixin_name, expr) storage = @eval(__module__, Stipple.@var_storage($expr)) delete!.(RefValue(storage), [:channel__, Stipple.AUTOFIELDS...]) + mixins = pop!(storage, :mixins__) quote Base.@kwdef struct $mixin_name diff --git a/src/stipple/rendering.jl b/src/stipple/rendering.jl index 3ac02bca..bcc1514c 100644 --- a/src/stipple/rendering.jl +++ b/src/stipple/rendering.jl @@ -1,3 +1,9 @@ +struct Mixin + M::DataType + prefix::String + postfix::String +end + """ js_print(io::IO, x) @@ -23,6 +29,7 @@ Parameters: - `pre`: preprocessor function that is applied to the resulting string of each element - `stip_delimiter`: If true strips a potential delimiter at the end of a string - `pre_delim`: preprocessor function for delimiter, if `nothing` it defaults to `pre` +- `unique`: if true, remove duplicates before joining ### Example ``` @@ -39,8 +46,38 @@ julia> join_js([1, f, "2 "], " - ", pre = strip) "1 - hi - 2" ``` """ -function join_js(xx::Union{Tuple, AbstractArray}, delim = ""; skip_empty = true, pre::Function = identity, strip_delimiter = true, pre_delim::Union{Function,Nothing} = nothing) - io = IOBuffer() +function join_js(xx::Union{Tuple, AbstractArray}, delim = ""; + skip_empty = true, + pre::Function = identity, + strip_delimiter = true, + pre_delim::Union{Function,Nothing} = nothing, + unique = false, +) + a = collect_js(xx, delim; skip_empty, pre, strip_delimiter, pre_delim, unique) + join(a, delim) +end + +function flatten(arr) + rst = Any[] + grep(v) = for x in v + if isa(x, Tuple) || isa(x, Array) + grep(x) + else push!(rst, x) end + end + grep(arr) + rst +end + +function collect_js(xx::Union{Tuple, AbstractArray}, delim = ""; + skip_empty::Bool = true, + pre::Function = identity, + strip_delimiter::Bool = true, + pre_delim::Union{Function,Nothing} = nothing, + unique::Bool = false, + key_replacement::Function = identity, +) + xx = flatten(xx) + a = String[] firstrun = true s_delim = pre_delim === nothing ? pre(delim) : pre_delim(delim) n_delim = ncodeunits(s_delim) @@ -48,7 +85,7 @@ function join_js(xx::Union{Tuple, AbstractArray}, delim = ""; skip_empty = true, x = x_raw isa Base.Callable ? x_raw() : x_raw io2 = IOBuffer() if x isa Union{AbstractDict, Pair, Base.Iterators.Pairs, Vector{<:Pair}} - s = json(Dict(k => JSONText(v) for (k, v) in (x isa Pair ? [x] : x)))[2:end - 1] + s = json(Dict(key_replacement(k) => JSONText(v) for (k, v) in (x isa Pair ? [x] : x)))[2:end - 1] print(io2, s) elseif x isa JSONText print(io2, x.s) @@ -58,17 +95,13 @@ function join_js(xx::Union{Tuple, AbstractArray}, delim = ""; skip_empty = true, s = String(take!(io2)) hasdelimiter = strip_delimiter && endswith(s, delim) s = pre(hasdelimiter ? s[1:end - ncodeunits(delim)] : s) - # if delimter has been removed already don't check for pretreated delimiter - firstrun || (skip_empty && isempty(s)) || print(io, delim) - # if first was not printed, firstrun stays true - firstrun && (firstrun = (skip_empty && isempty(s))) - print(io, strip_delimiter && ! hasdelimiter && endswith(s, s_delim) ? s[1:end - n_delim] : s) + (skip_empty && isempty(s)) || push!(a, s) end - String(take!(io)) + unique ? unique!(a) : a end -function join_js(x, delim = ""; skip_empty = true, pre::Function = identity, strip_delimiter = true, pre_delim::Union{Function,Nothing} = nothing) - join_js([x], delim; skip_empty, pre, strip_delimiter, pre_delim) +function join_js(x, delim = ""; skip_empty = true, pre::Function = identity, strip_delimiter = true, pre_delim::Union{Function,Nothing} = nothing, unique = false) + join_js([x], delim; skip_empty, pre, strip_delimiter, pre_delim, unique) end const RENDERING_MAPPINGS = Dict{String,String}() @@ -128,36 +161,69 @@ const MIXINS = RefValue(["watcherMixin", "reviveMixin", "eventMixin", "filterMix add_mixins(mixins::Vector{String}) = union!(push!(MIXINS[], mixins...)) add_mixins(mixin::String) = union!(push!(MIXINS[], mixin)) -""" - function Stipple.render(app::M, fieldname::Union{Symbol,Nothing} = nothing)::Dict{Symbol,Any} where {M<:ReactiveModel} +function mixins(::Type{<:ReactiveModel}) + Mixin[] +end +Stipple.mixins(::T) where T <: ReactiveModel = Stipple.mixins(T) -Renders the Julia `ReactiveModel` `app` as the corresponding Vue.js JavaScript code. -""" -function Stipple.render(app::M)::Dict{Symbol,Any} where {M<:ReactiveModel} - result = OptDict() +function get_known_js_vars(::Type{M}) where M<:ReactiveModel + CM = Stipple.get_concrete_type(M) + vars = vcat(setdiff(fieldnames(CM), Stipple.AUTOFIELDS, Stipple.INTERNALFIELDS), Symbol.(keys(client_data(CM)))) + + computed_vars = Symbol.(strip.(first.(split.(collect_js([js_methods(M)]), ':', limit = 2)), '"')) + method_vars = Symbol.(strip.(first.(split.(collect_js([js_computed(M)]), ':', limit = 2)), '"')) - for field in fieldnames(typeof(app)) - f = getfield(app, field) + vars = vcat(vars, computed_vars, method_vars) + sort!(sort!(vars), by = x->length(String(x)), rev = true) +end - occursin(SETTINGS.private_pattern, String(field)) && continue - f isa Reactive && f.r_mode == PRIVATE && continue +function get_known_js_vars(::Type{T}) where T + Symbol[fieldnames(T)...] +end - result[field] = Stipple.jsrender(f, field) +function js_mixin(m::Mixin, js_f, delim) + M, prefix, postfix = m.M, m.prefix, m.postfix + vars = get_known_js_vars(M) + empty_var = Symbol("") ∈ vars + empty_var && setdiff!(vars, [Symbol("")]) + + replace_rule1 = Regex("\\b(this|GENIEMODEL)\\.($(join(vars, '|')))\\b") => SubstitutionString("\\1.$prefix\\2$postfix") + replace_rule2 = Regex("\\b(this|GENIEMODEL)\\. ") => SubstitutionString("\\1.$prefix$postfix ") + + no_modifiers = isempty(prefix) && isempty(postfix) || js_f ∉ (js_methods, js_computed, js_watch) + add_fixes(s) = Symbol(prefix, s, postfix) + add_fixes(s::JSONText) = Symbol(s.s) + xx = collect_js([js_f(M)], delim; pre = strip, key_replacement = no_modifiers ? identity : add_fixes) + + isempty(prefix) && isempty(postfix) && return xx + + for i in eachindex(xx) + s = replace(xx[i], replace_rule1) + empty_var && (s = replace(s, replace_rule2)) + xx[i] = s end + + return xx +end - # convert :data to () => ({ }) - data = json(merge(result, client_data(app))) +function render_js_options!(::Union{M, Type{M}}, vue::Dict{Symbol, Any} = Dict{Symbol, Any}(); mixin = false, indent = 4) where {M<:ReactiveModel} + indent isa Integer && (indent = repeat(" ", indent)) + pre = isempty(indent) ? strip : s -> replace(strip(s), "\n" => "\n$indent") + sep1 = ",\n\n$indent" + sep2 = "\n\n$indent" - vue = Dict( - :mixins => JSONText.(MIXINS[]), - :data => JSONText("() => ($data)") - ) for (f, field) in ((js_methods, :methods), (js_computed, :computed), (js_watch, :watch)) - js = join_js(f(app), ",\n "; pre = strip) + xx = Any[f(M)] + for m in mixins(M) + push!(xx, js_mixin(m, f, sep1)) + end + if field == :watch - watch_auto = join_js(Stipple.js_watch_auto(app), ",\n "; pre = strip) - watch_auto == "" || (js = join_js([js, watch_auto], ",\n ")) + watch_auto = js_watch_auto(M) + isempty(watch_auto) || push!(xx, watch_auto) end + + js = join_js(xx, sep1; pre, unique = true) isempty(js) || push!(vue, field => JSONText("{\n $js\n}")) end @@ -166,20 +232,55 @@ function Stipple.render(app::M)::Dict{Symbol,Any} where {M<:ReactiveModel} (js_before_update, :beforeUpdate), (js_updated, :updated), (js_activated, :activated), (js_deactivated, :deactivated), (js_before_destroy, :beforeDestroy), (js_destroyed, :destroyed), (js_error_captured, :errorCaptured),) - js = join_js(f(app), "\n\n "; pre = strip) + xx = Any[f(M)] + for m in mixins(M) + push!(xx, js_mixin(m, f, sep2)) + end + if field == :created - created_auto = join_js(Stipple.js_created_auto(app), "\n\n "; pre = strip) - created_auto == "" || (js = join_js([js, created_auto], "\n\n ")) - elseif field == :mounted + created_auto = Stipple.js_created_auto(M) + isempty(created_auto) || push!(xx, created_auto) + elseif field == :mounted && ! mixin mounted_auto = """setTimeout(() => { this.WebChannel.unsubscriptionHandlers.push(() => this.handle_event({}, 'finalize')) console.log('Unsubscription handler installed') }, 100) """ - js = join_js([js, mounted_auto], "\n\n ") + push!(xx, mounted_auto) end + + js = join_js(xx, sep2; pre, unique = true) isempty(js) || push!(vue, field => JSONText("function(){\n $js\n}")) end + vue +end + +""" + function Stipple.render(app::M, fieldname::Union{Symbol,Nothing} = nothing)::Dict{Symbol,Any} where {M<:ReactiveModel} + +Renders the Julia `ReactiveModel` `app` as the corresponding Vue.js JavaScript code. +""" +function Stipple.render(app::M)::Dict{Symbol,Any} where {M<:ReactiveModel} + result = OptDict() + + for field in fieldnames(typeof(app)) + f = getfield(app, field) + + occursin(SETTINGS.private_pattern, String(field)) && continue + f isa Reactive && f.r_mode == PRIVATE && continue + + result[field] = Stipple.jsrender(f, field) + end + + # convert :data to () => ({ }) + data = json(merge(result, client_data(app))) + + vue = Dict( + :mixins => JSONText.(MIXINS[]), + :data => JSONText("() => ($data)") + ) + + render_js_options!(app, vue) vue end