From 22e8eb249f0e835fdc8e7d30dc4acdd99c7b3411 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Helmut=20H=C3=A4nsel?= Date: Sun, 27 Apr 2025 23:52:51 +0200 Subject: [PATCH 01/13] refactor jsmethods to also work for types --- src/ReactiveTools.jl | 10 +++--- src/stipple/jsmethods.jl | 68 +++++++++++++++++++++++----------------- 2 files changed, 44 insertions(+), 34 deletions(-) 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..10827fe7 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(::Type{<:ReactiveModel})::String = "" +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(::Type{<:ReactiveModel})::String = "" +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(::Type{<:ReactiveModel})::String = "" +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(::Type{<:ReactiveModel})::String = "" + $f(::T) where T = $f(T) end) end @@ -254,11 +264,11 @@ function js_initscript(initscript::String) end function js_created_auto(x) -"" + "" end function js_watch_auto(x) -"" + "" end # methods to be used directly as arguments to js_methods From 58cf601b351ab32aa12ae94277214b8f5f1fe94e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Helmut=20H=C3=A4nsel?= Date: Wed, 7 May 2025 00:31:37 +0200 Subject: [PATCH 02/13] add pre- and postfixes for js_methods, etc. --- src/Elements.jl | 6 +- src/stipple/reactivity.jl | 151 +++++++++++++++++++-------------- src/stipple/rendering.jl | 171 +++++++++++++++++++++++++++++--------- 3 files changed, 227 insertions(+), 101 deletions(-) diff --git a/src/Elements.jl b/src/Elements.jl index 81590750..103dc7b9 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/stipple/reactivity.jl b/src/stipple/reactivity.jl index 6798d489..d2a5ebe5 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, Ref(required_vars), Ref(all_vars)) add_brackets!.(expr.args, Ref(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 = 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 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__) + 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!.(Ref(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 08242a3e..f0243f49 100644 --- a/src/stipple/rendering.jl +++ b/src/stipple/rendering.jl @@ -1,3 +1,9 @@ +struct Mixin + M::Type{<:ReactiveModel} + 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,65 @@ const MIXINS = Ref(["watcherMixin", "reviveMixin", "eventMixin", "filterMixin"]) 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} - -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 mixins(::Type{<:ReactiveModel}) + Mixin[] +end +Stipple.mixins(::T) where T <: ReactiveModel = Stipple.mixins(T) - for field in fieldnames(typeof(app)) - f = getfield(app, field) +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)), '"')) - occursin(SETTINGS.private_pattern, String(field)) && continue - f isa Reactive && f.r_mode == PRIVATE && continue + vars = vcat(vars, computed_vars, method_vars) + sort!(sort!(vars), by = x->length(String(x)), rev = true) +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) + + no_modifiers && 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 = strip(js_watch_auto(M)) + 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 +228,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 = strip(Stipple.js_created_auto(M)) + 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 From 11f2759a37618792b2c61f8cc0f950d0783c3ab9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Helmut=20H=C3=A4nsel?= Date: Wed, 7 May 2025 01:15:17 +0200 Subject: [PATCH 03/13] fix renaming --- src/stipple/reactivity.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stipple/reactivity.jl b/src/stipple/reactivity.jl index e4a86a2f..f3a27c18 100644 --- a/src/stipple/reactivity.jl +++ b/src/stipple/reactivity.jl @@ -507,12 +507,12 @@ macro var_storage(expr, handler = nothing) 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 = mixin) + 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(mixin, :var"!_handlers_expr") + 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] From 277511a751370ab48de5bf8f5d522bb4f6cf2f69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Helmut=20H=C3=A4nsel?= Date: Thu, 8 May 2025 07:26:08 +0200 Subject: [PATCH 04/13] fix replacements for mixins --- src/stipple/rendering.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stipple/rendering.jl b/src/stipple/rendering.jl index fa180812..dd3614e2 100644 --- a/src/stipple/rendering.jl +++ b/src/stipple/rendering.jl @@ -191,7 +191,7 @@ function js_mixin(m::Mixin, js_f, delim) add_fixes(s::JSONText) = Symbol(s.s) xx = collect_js([js_f(M)], delim; pre = strip, key_replacement = no_modifiers ? identity : add_fixes) - no_modifiers && return xx + isempty(prefix) && isempty(postfix) && return xx for i in eachindex(xx) s = replace(xx[i], replace_rule1) From d2fe2434646284d1da80b04625a4f8a5aa7efef6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Helmut=20H=C3=A4nsel?= Date: Fri, 9 May 2025 07:29:39 +0200 Subject: [PATCH 05/13] allow js_methods etc. for any DataType, fix js_created_auto and js_watch_auto --- src/stipple/jsmethods.jl | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/stipple/jsmethods.jl b/src/stipple/jsmethods.jl index 10827fe7..9c151a0c 100644 --- a/src/stipple/jsmethods.jl +++ b/src/stipple/jsmethods.jl @@ -32,7 +32,7 @@ js_bye() = :bye => "function() {console.log('Bye!')}" js_methods(::Type{<:MyDashboard}) = [js_greet, js_bye] ``` """ -js_methods(::Type{<:ReactiveModel})::String = "" +js_methods(::DataType) = "" js_methods(::T) where T = js_methods(T) # deprecated, now part of the model @@ -71,7 +71,7 @@ js_computed(app::MyDashboard) = \"\"\" \"\"\" ``` """ -js_computed(::Type{<:ReactiveModel})::String = "" +js_computed(::DataType) = "" js_computed(::T) where T = js_computed(T) const jscomputed = js_computed @@ -103,7 +103,7 @@ js_watch(::Type{<:MyDashboard}) = \"\"\" \"\"\" ``` """ -js_watch(::Type{<:ReactiveModel})::String = "" +js_watch(::DataType) = "" js_watch(::T) where T = js_watch(T) const jswatch = js_watch @@ -263,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 From b57aa20b1e4a0dda58a1c9c3f97de17023ef2062 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Helmut=20H=C3=A4nsel?= Date: Fri, 9 May 2025 07:36:56 +0200 Subject: [PATCH 06/13] fixes: extend Mixin type to all DataTypes, evaluate Mixins in scope of calling module --- src/stipple/reactivity.jl | 2 +- src/stipple/rendering.jl | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/stipple/reactivity.jl b/src/stipple/reactivity.jl index 2a23f476..accba9b1 100644 --- a/src/stipple/reactivity.jl +++ b/src/stipple/reactivity.jl @@ -615,7 +615,7 @@ macro type(modelname, storage) storage = @eval __module__ $storage mixins = if haskey(storage, :mixins__) - eval(pop!(storage, :mixins__)) + __module__.eval(pop!(storage, :mixins__)) else Mixin[] end diff --git a/src/stipple/rendering.jl b/src/stipple/rendering.jl index dd3614e2..57899bf1 100644 --- a/src/stipple/rendering.jl +++ b/src/stipple/rendering.jl @@ -1,5 +1,5 @@ struct Mixin - M::Type{<:ReactiveModel} + M::DataType prefix::String postfix::String end @@ -177,6 +177,10 @@ function get_known_js_vars(::Type{M}) where M<:ReactiveModel sort!(sort!(vars), by = x->length(String(x)), rev = true) end +function get_known_js_vars(::Type{T}) where T + Symbol[fieldnames(T)...] +end + function js_mixin(m::Mixin, js_f, delim) M, prefix, postfix = m.M, m.prefix, m.postfix vars = get_known_js_vars(M) From 018d0fa172156f872b124ca20de50a2ebc21e868 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Helmut=20H=C3=A4nsel?= Date: Fri, 9 May 2025 07:47:48 +0200 Subject: [PATCH 07/13] fix js_xxx_auto: cover both strings and arrays --- src/stipple/rendering.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/stipple/rendering.jl b/src/stipple/rendering.jl index 57899bf1..bcc1514c 100644 --- a/src/stipple/rendering.jl +++ b/src/stipple/rendering.jl @@ -219,8 +219,8 @@ function render_js_options!(::Union{M, Type{M}}, vue::Dict{Symbol, Any} = Dict{S end if field == :watch - watch_auto = strip(js_watch_auto(M)) - watch_auto == "" || push!(xx, watch_auto) + watch_auto = js_watch_auto(M) + isempty(watch_auto) || push!(xx, watch_auto) end js = join_js(xx, sep1; pre, unique = true) @@ -238,8 +238,8 @@ function render_js_options!(::Union{M, Type{M}}, vue::Dict{Symbol, Any} = Dict{S end if field == :created - created_auto = strip(Stipple.js_created_auto(M)) - created_auto == "" || push!(xx, created_auto) + 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')) From 49f595ace3c2dfd66f1da1b14be94569bf9dfa5c Mon Sep 17 00:00:00 2001 From: hhaensel Date: Thu, 15 May 2025 08:45:56 +0200 Subject: [PATCH 08/13] allow js_methods etc. for any DataType part II --- src/stipple/jsmethods.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stipple/jsmethods.jl b/src/stipple/jsmethods.jl index 9c151a0c..41a5d4c2 100644 --- a/src/stipple/jsmethods.jl +++ b/src/stipple/jsmethods.jl @@ -180,7 +180,7 @@ for (f, field) in ( $($f)(::MyDashboard) = [startcamera, stopcamera] ``` """ - $f(::Type{<:ReactiveModel})::String = "" + $f(::DataType)::String = "" $f(::T) where T = $f(T) end) end From c2f8afe02e4bcd02cc46e6d528c3ada4da8d117f Mon Sep 17 00:00:00 2001 From: hhaensel <31985040+hhaensel@users.noreply.github.com> Date: Tue, 16 Sep 2025 23:09:16 +0200 Subject: [PATCH 09/13] Update src/stipple/rendering.jl Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/stipple/rendering.jl | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/stipple/rendering.jl b/src/stipple/rendering.jl index bcc1514c..f7dd726d 100644 --- a/src/stipple/rendering.jl +++ b/src/stipple/rendering.jl @@ -59,11 +59,13 @@ 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(v) = for x in v + if isa(x, Tuple) || isa(x, Array) + grep(x) + else + push!(rst, x) + end + end grep(arr) rst end From 028cf2d181cd01a0d33514a6a617e5ab8ac9a1ef Mon Sep 17 00:00:00 2001 From: hhaensel <31985040+hhaensel@users.noreply.github.com> Date: Tue, 16 Sep 2025 23:09:49 +0200 Subject: [PATCH 10/13] fix order of assignments Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/stipple/reactivity.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/stipple/reactivity.jl b/src/stipple/reactivity.jl index accba9b1..1226c16d 100644 --- a/src/stipple/reactivity.jl +++ b/src/stipple/reactivity.jl @@ -603,8 +603,9 @@ end macro type(modelname, storage) M = if modelname isa DataType + parent = parentmodule(modelname) modelname = modelname.name.name - parentmodule(modelname) + parent else __module__ end From f7a79d84152c84699ae7a0ff018e5404ccfad0ac Mon Sep 17 00:00:00 2001 From: hhaensel <31985040+hhaensel@users.noreply.github.com> Date: Tue, 16 Sep 2025 23:11:13 +0200 Subject: [PATCH 11/13] fix naming of var assignments Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/stipple/rendering.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stipple/rendering.jl b/src/stipple/rendering.jl index f7dd726d..11ef6631 100644 --- a/src/stipple/rendering.jl +++ b/src/stipple/rendering.jl @@ -172,8 +172,8 @@ 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)), '"')) + computed_vars = Symbol.(strip.(first.(split.(collect_js([js_computed(M)]), ':', limit = 2)), '"')) + method_vars = Symbol.(strip.(first.(split.(collect_js([js_methods(M)]), ':', limit = 2)), '"')) vars = vcat(vars, computed_vars, method_vars) sort!(sort!(vars), by = x->length(String(x)), rev = true) From ee95d46d4ad3dee80313258a17f63f0e417417cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Helmut=20H=C3=A4nsel?= Date: Tue, 16 Sep 2025 23:16:27 +0200 Subject: [PATCH 12/13] add comment in `@type`, add unique parameter in docstring of join_js --- src/stipple/reactivity.jl | 2 ++ src/stipple/rendering.jl | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/stipple/reactivity.jl b/src/stipple/reactivity.jl index 1226c16d..69ebe963 100644 --- a/src/stipple/reactivity.jl +++ b/src/stipple/reactivity.jl @@ -603,6 +603,8 @@ end macro type(modelname, storage) M = if modelname isa DataType + # this was necessary for nested macros or mixins, not sure whether this is still the case + # in this snippet, it is required: `@app MyApp @in x = 1; :(Stipple.@type $MyApp LittleDict()) |> eval` parent = parentmodule(modelname) modelname = modelname.name.name parent diff --git a/src/stipple/rendering.jl b/src/stipple/rendering.jl index 11ef6631..4406f39a 100644 --- a/src/stipple/rendering.jl +++ b/src/stipple/rendering.jl @@ -42,7 +42,7 @@ World julia> f() = "hi - "; -julia> join_js([1, f, "2 "], " - ", pre = strip) +julia> join_js([1, f, "2 ", 1], " - ", pre = strip, unique = true) "1 - hi - 2" ``` """ From dc09fa2b990706b8b43b1879b79a55a4a387e8b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Helmut=20H=C3=A4nsel?= Date: Tue, 16 Sep 2025 23:39:56 +0200 Subject: [PATCH 13/13] fix type in render_js_options!() --- src/stipple/rendering.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stipple/rendering.jl b/src/stipple/rendering.jl index 59dd5ce5..82fdfa86 100644 --- a/src/stipple/rendering.jl +++ b/src/stipple/rendering.jl @@ -208,7 +208,7 @@ function js_mixin(m::Mixin, js_f, delim) return xx end -function render_js_options!(::Union{M, Type{M}}, vue::Dict{Symbol, Any} = Dict{Symbol, Any}(); mixin = false, indent = 4) where {M<:ReactiveModel} +function render_js_options!(::Union{M, Type{M}}, vue::OrderedDict{Symbol, Any} = OrderedDict{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"