diff --git a/src/ReactiveTools.jl b/src/ReactiveTools.jl index 3dd6e018..2ca612c4 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,6 +532,56 @@ macro mixin(location, expr, prefix, postfix) end |> esc end +""" + @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 + #===# function init_handlers(m::Module) @@ -606,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) diff --git a/src/Stipple.jl b/src/Stipple.jl index 7e19645f..ab402144 100644 --- a/src/Stipple.jl +++ b/src/Stipple.jl @@ -761,6 +761,14 @@ 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 + out = mixin_dep(mixin; mode)() + out === nothing || push!(output, out) + end output end @@ -809,6 +817,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..4bdf49f1 100644 --- a/src/stipple/rendering.jl +++ b/src/stipple/rendering.jl @@ -119,10 +119,11 @@ 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}() - - for field in fieldnames(typeof(app)) +function Stipple.render(app::M; mode::Symbol = :vue)::LittleDict{Symbol,Any} where {M<:ReactiveModel} + result = LittleDict{String,Any}() + 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 @@ -131,9 +132,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 +160,24 @@ 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{Type{<:ReactiveModel}} + get_mixins(get_abstract_type(typeof(app))) +end + +function mixin_dep(Mixin::Type{<:ReactiveModel}; mode::Symbol = :mixindeps) + mix = strip(json(render(Mixin(); mode)), '"') + mixin_dep() = if mix == "{}" + nothing + else + script("const $(nameof(Mixin)) = $mix\n") + end +end + + """ function Stipple.render(val::T, fieldname::Union{Symbol,Nothing} = nothing) where {T}