From 83b3247a169870ad2d0986cc93b0eeb6ff5ba045 Mon Sep 17 00:00:00 2001 From: hhaensel Date: Wed, 29 Nov 2023 11:57:49 +0100 Subject: [PATCH 1/5] support ReactiveModels as mixins --- src/ReactiveTools.jl | 4 ++++ src/Stipple.jl | 8 ++++++++ src/stipple/rendering.jl | 31 ++++++++++++++++++++++++++----- 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/src/ReactiveTools.jl b/src/ReactiveTools.jl index 3dd6e018..895f0587 100644 --- a/src/ReactiveTools.jl +++ b/src/ReactiveTools.jl @@ -532,6 +532,10 @@ macro mixin(location, expr, prefix, postfix) end |> esc end +macro mixins(location, mixins) + :(Stipple.get_mixins(::Type{$location}) = Type{<:ReactiveModel}[$mixins...]) |> esc +end + #===# function init_handlers(m::Module) diff --git a/src/Stipple.jl b/src/Stipple.jl index 7e19645f..f6f9a138 100644 --- a/src/Stipple.jl +++ b/src/Stipple.jl @@ -761,6 +761,13 @@ function injectdeps(output::Vector{AbstractString}, M::Type{<:ReactiveModel}) :: startswith("$key", model_prefix) && push!(output, f()...) end end + for mixin in get_mixins(M) + modelfields = fieldnames(Stipple.get_concrete_type(M)) + mixinfields = fieldnames(Stipple.get_concrete_type(mixin)) + # if all fields are already part of the model, don't include data + mode = isempty(setdiff(mixinfields, modelfields)) ? :mixindeps : :mixin + push!(output, mixin_dep(mixin; mode)()) + end output end @@ -809,6 +816,7 @@ function deps!(M::Type{<:ReactiveModel}, f::Function; extra_deps = true) end deps!(M::Type{<:ReactiveModel}, modul::Module; extra_deps = true) = deps!(M, modul.deps; extra_deps) +deps!(M::Type{<:ReactiveModel}, mixin::ReactiveModel; extra_deps = true) = deps!(M, mixin_deps(mixin); extra_deps) deps!(m::Any, v::Vector{Union{Function, Module}}) = deps!.(Ref(m), v) deps!(m::Any, t::Tuple) = [deps!(m, f) for f in t] diff --git a/src/stipple/rendering.jl b/src/stipple/rendering.jl index c57d34eb..d6d07cfe 100644 --- a/src/stipple/rendering.jl +++ b/src/stipple/rendering.jl @@ -119,8 +119,8 @@ jsrender(r::Reactive, args...) = jsrender(getfield(getfield(r,:o), :val), args.. Renders the Julia `ReactiveModel` `app` as the corresponding Vue.js JavaScript code. """ -function Stipple.render(app::M)::Dict{Symbol,Any} where {M<:ReactiveModel} - result = Dict{String,Any}() +function Stipple.render(app::M; mode::Symbol = :vue)::LittleDict{Symbol,Any} where {M<:ReactiveModel} + result = LittleDict{String,Any}() for field in fieldnames(typeof(app)) f = getfield(app, field) @@ -131,9 +131,17 @@ function Stipple.render(app::M)::Dict{Symbol,Any} where {M<:ReactiveModel} result[julia_to_vue(field)] = Stipple.jsrender(f, field) end - vue = Dict( :el => JSONText("rootSelector"), - :mixins => JSONText("[watcherMixin, reviveMixin, eventMixin]"), - :data => merge(result, client_data(app))) + vue = LittleDict{Symbol, Any}() + mixin_names = Symbol[nameof(m) for m in get_mixins(app)] + + if mode == :vue + push!(vue, :el => JSONText("rootSelector")) + mixin_names = vcat([:watcherMixin, :reviveMixin, :eventMixin], mixin_names) + end + + isempty(mixin_names) || push!(vue, :mixins => JSONText("[$(join(mixin_names, ", "))]")) + mode == :mixindeps || push!(vue, :data => merge(result, client_data(app))) + for (f, field) in ((components, :components), (js_methods, :methods), (js_computed, :computed), (js_watch, :watch)) js = join_js(f(app), ",\n "; pre = strip) isempty(js) || push!(vue, field => JSONText("{\n $js\n}")) @@ -151,6 +159,19 @@ function Stipple.render(app::M)::Dict{Symbol,Any} where {M<:ReactiveModel} vue end +function get_mixins(::Type{<:ReactiveModel})::Vector{Type{ReactiveModel}} + Type{ReactiveModel}[] +end + +function get_mixins(app::ReactiveModel)::Vector{ReactiveModel} + get_mixins(get_abstract_type(typeof(app))) +end + +function mixin_dep(Mixin::Type{<:ReactiveModel}; mode::Symbol = :mixindeps) + mixin_dep() = script("const $(nameof(Mixin)) = $(strip(json(render(Mixin(); mode)), '"'))\n") +end + + """ function Stipple.render(val::T, fieldname::Union{Symbol,Nothing} = nothing) where {T} From d74697742ac00482cac159e8bb4744e008d97644 Mon Sep 17 00:00:00 2001 From: hhaensel Date: Wed, 29 Nov 2023 22:57:37 +0100 Subject: [PATCH 2/5] exclude autofields from mixin rendering --- src/stipple/rendering.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/stipple/rendering.jl b/src/stipple/rendering.jl index d6d07cfe..a930e4ac 100644 --- a/src/stipple/rendering.jl +++ b/src/stipple/rendering.jl @@ -121,8 +121,9 @@ Renders the Julia `ReactiveModel` `app` as the corresponding Vue.js JavaScript c """ function Stipple.render(app::M; mode::Symbol = :vue)::LittleDict{Symbol,Any} where {M<:ReactiveModel} result = LittleDict{String,Any}() - - for field in fieldnames(typeof(app)) + ff = collect(fieldnames(typeof(app))) + mode == :vue || setdiff!(ff, [:channel__, :modes__], Stipple.AUTOFIELDS) + for field in ff f = getfield(app, field) occursin(SETTINGS.private_pattern, String(field)) && continue From bac3368fe58527a777e4e65d4f67cb326e9d176c Mon Sep 17 00:00:00 2001 From: hhaensel Date: Wed, 29 Nov 2023 22:58:07 +0100 Subject: [PATCH 3/5] exclude empty mixins from being included --- src/Stipple.jl | 3 ++- src/stipple/rendering.jl | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Stipple.jl b/src/Stipple.jl index f6f9a138..ab402144 100644 --- a/src/Stipple.jl +++ b/src/Stipple.jl @@ -766,7 +766,8 @@ function injectdeps(output::Vector{AbstractString}, M::Type{<:ReactiveModel}) :: mixinfields = fieldnames(Stipple.get_concrete_type(mixin)) # if all fields are already part of the model, don't include data mode = isempty(setdiff(mixinfields, modelfields)) ? :mixindeps : :mixin - push!(output, mixin_dep(mixin; mode)()) + out = mixin_dep(mixin; mode)() + out === nothing || push!(output, out) end output end diff --git a/src/stipple/rendering.jl b/src/stipple/rendering.jl index a930e4ac..5fb8b10b 100644 --- a/src/stipple/rendering.jl +++ b/src/stipple/rendering.jl @@ -169,7 +169,12 @@ function get_mixins(app::ReactiveModel)::Vector{ReactiveModel} end function mixin_dep(Mixin::Type{<:ReactiveModel}; mode::Symbol = :mixindeps) - mixin_dep() = script("const $(nameof(Mixin)) = $(strip(json(render(Mixin(); mode)), '"'))\n") + mix = strip(json(render(Mixin(); mode)), '"') + mixin_dep() = if mix == "{}" + nothing + else + script("const $(nameof(Mixin)) = $mix\n") + end end From fa1898f74a661c4673f49d722cb4018f8694445d Mon Sep 17 00:00:00 2001 From: hhaensel Date: Thu, 30 Nov 2023 00:22:03 +0100 Subject: [PATCH 4/5] fix return type of get_mixins, export `@mixins` add docstring --- src/ReactiveTools.jl | 52 +++++++++++++++++++++++++++++++++++++--- src/stipple/rendering.jl | 4 ++-- 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/src/ReactiveTools.jl b/src/ReactiveTools.jl index 895f0587..b65ab4a7 100644 --- a/src/ReactiveTools.jl +++ b/src/ReactiveTools.jl @@ -8,7 +8,7 @@ import Genie import Stipple: deletemode!, parse_expression!, init_storage # definition of variables -export @readonly, @private, @in, @out, @jsfn, @readonly!, @private!, @in!, @out!, @jsfn!, @mixin +export @readonly, @private, @in, @out, @jsfn, @readonly!, @private!, @in!, @out!, @jsfn!, @mixin, @mixins #definition of handlers/events export @onchange, @onbutton, @event, @notify @@ -532,8 +532,54 @@ macro mixin(location, expr, prefix, postfix) end |> esc end -macro mixins(location, mixins) - :(Stipple.get_mixins(::Type{$location}) = Type{<:ReactiveModel}[$mixins...]) |> esc +""" + @mixins [DemoMixin1, DemoMixin2] + +Add one or more ReactiveModels as mixin to the app. The mixins need to be passed as a Vector. +This feature is meant to be able to design functionalities that can be reused within other apps. + +All fields, client data, and js functions like watchers, life cycle hooks, etc are automatically embedded. + +However, the recommended way of embedding the fields +is via the `@mixin` in the app definition, because only then they are automatically synchronised according to their definition. +So in order to include a mixin with data and functions, two steps are needed. + +This feature is still tentative and might be redesigned in the future. +```julia +@app GreetMixin begin + @in name = "John Doe" +end + +@mounted GreetMixin = "console.log('Just mounted the App including the GreetMixin')" + +@methods GreetMixin \"\"\" +greet: function() { console.log('Hi ' + this.name + '!') } +\"\"\" + +@mixins [GreetMixin] + +@app begin + @mixin GreetMixin + @in s = "Hi" + @in i = 10 +end + +ui() = btn("Say Hello", @click("greet")) + +@page("/", ui()) + +up(open_browser = true) +""" +macro mixins(expr) + esc(quote + let M = Stipple.@type + Stipple.ReactiveTools.@mixins M $expr + end + end) +end + +macro mixins(App, mixins) + :(Stipple.get_mixins(::Type{<:$App}) = Type{<:ReactiveModel}[$mixins...]) |> esc end #===# diff --git a/src/stipple/rendering.jl b/src/stipple/rendering.jl index 5fb8b10b..4bdf49f1 100644 --- a/src/stipple/rendering.jl +++ b/src/stipple/rendering.jl @@ -160,11 +160,11 @@ function Stipple.render(app::M; mode::Symbol = :vue)::LittleDict{Symbol,Any} whe vue end -function get_mixins(::Type{<:ReactiveModel})::Vector{Type{ReactiveModel}} +function get_mixins(::Type{<:ReactiveModel})::Vector{Type{<:ReactiveModel}} Type{ReactiveModel}[] end -function get_mixins(app::ReactiveModel)::Vector{ReactiveModel} +function get_mixins(app::ReactiveModel)::Vector{Type{<:ReactiveModel}} get_mixins(get_abstract_type(typeof(app))) end From a046bbbd9932642d2bc6819bbb406057163c5c99 Mon Sep 17 00:00:00 2001 From: hhaensel Date: Fri, 1 Dec 2023 10:05:19 +0100 Subject: [PATCH 5/5] add mixin handlers on the backend --- src/ReactiveTools.jl | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/ReactiveTools.jl b/src/ReactiveTools.jl index b65ab4a7..2ca612c4 100644 --- a/src/ReactiveTools.jl +++ b/src/ReactiveTools.jl @@ -656,8 +656,13 @@ macro init(args...) identity end end - instance = let model = initfn($(init_args...)) - new_handlers ? Base.invokelatest(handlersfn, model) : handlersfn(model) + instance = initfn($(init_args...)) + # append eventhandlers + new_handlers ? Base.invokelatest(handlersfn, instance) : handlersfn(instance) + # append eventhandlers of mixins + for mixin in Stipple.get_mixins(instance) + handlers = get(Stipple.ReactiveTools.HANDLERS_FUNCTIONS, mixin, nothing) + handlers === nothing || handlers(instance) end for p in Stipple.Pages._pages p.context == $__module__ && (p.model = instance)