diff --git a/assets/js/keepalive.js b/assets/js/keepalive.js index 449e890b..58200dc2 100644 --- a/assets/js/keepalive.js +++ b/assets/js/keepalive.js @@ -1,21 +1,33 @@ /* -** keepalive.js // v1.0.0 // 6th January 2022 +** keepalive.js // v1.1.0 // 11 November 2024 ** Keeps alive the websocket connection by sending a ping every x seconds ** where x = Genie.config.webchannels_keepalive_frequency */ -function keepalive() { - if (window._lastMessageAt !== undefined) { - if (Date.now() - window._lastMessageAt < Genie.Settings.webchannels_keepalive_frequency) { - return +function keepalive(WebChannel) { + if (WebChannel.lastMessageAt !== undefined) { + dt = Date.now() - WebChannel.lastMessageAt; + // allow for a 200ms buffer + if (dt + 200 < Genie.Settings.webchannels_keepalive_frequency) { + keepaliveTimer(WebChannel, Genie.Settings.webchannels_keepalive_frequency - dt); + return; } } - if (Genie.Settings.env == 'dev') { - console.info('Keeping connection alive'); + if (!WebChannel.ws_disconnected) { + if (Genie.Settings.env == 'dev') { + console.info('Keeping connection alive'); + } + WebChannel.sendMessageTo(WebChannel.channel, 'keepalive', { + 'payload': {} + }); } - - Genie.WebChannels.sendMessageTo(CHANNEL, 'keepalive', { - 'payload': {} - }); } + +function keepaliveTimer(WebChannel, startDelay = Genie.Settings.webchannels_keepalive_frequency) { + clearInterval(WebChannel.keepalive_interval); + setTimeout(() => { + keepalive(WebChannel); + WebChannel.keepalive_interval = setInterval(() => keepalive(WebChannel), Genie.Settings.webchannels_keepalive_frequency); + }, startDelay) +} \ No newline at end of file diff --git a/assets/js/watchers.js b/assets/js/watchers.js index 6d87f2d6..9f973cc9 100644 --- a/assets/js/watchers.js +++ b/assets/js/watchers.js @@ -67,7 +67,7 @@ const watcherMixin = { }, push: function (field) { - Genie.WebChannels.sendMessageTo(CHANNEL, 'watchers', {'payload': { + this.WebChannel.sendMessageTo(this.channel_, 'watchers', {'payload': { 'field': field, 'newval': this[field], 'oldval': null, @@ -110,7 +110,7 @@ const eventMixin = { if (event_data === undefined) { event_data = {} } console.debug('event: ' + JSON.stringify(event_data) + ":" + event_handler) if (mode=='addclient') { event_data._addclient = true} - Genie.WebChannels.sendMessageTo(window.CHANNEL, 'events', { + this.WebChannel.sendMessageTo(this.channel_, 'events', { 'event': { 'name': event_handler, 'event': event_data diff --git a/src/Elements.jl b/src/Elements.jl index 08c61980..4f4947c7 100644 --- a/src/Elements.jl +++ b/src/Elements.jl @@ -130,7 +130,6 @@ function vue_integration(::Type{M}; debounce::Int = Stipple.JS_DEBOUNCE_TIME, transport::Module = Genie.WebChannels)::String where {M<:ReactiveModel} model = Base.invokelatest(M) - vue_app = json(model |> Stipple.render) vue_app = replace(vue_app, "\"$(getchannel(model))\"" => Stipple.channel_js_name) @@ -157,89 +156,88 @@ function vue_integration(::Type{M}; output = string( - " - - function initStipple(rootSelector){ - // components = Stipple.init($( core_theme ? "{theme: '$theme'}" : "" )); - const app = Vue.createApp($( replace(vue_app, "'$(Stipple.UNDEFINED_PLACEHOLDER)'"=>Stipple.UNDEFINED_VALUE) )) - /* Object.entries(components).forEach(([key, value]) => { - app.component(key, value) - }); */ - Stipple.init( app, $( core_theme ? "{theme: '$theme'}" : "" )); - $globalcomps - $comps - // gather legacy global options - app.prototype = {} - $(plugins(M)) - // apply legacy global options - Object.entries(app.prototype).forEach(([key, value]) => { - app.config.globalProperties[key] = value - }); - window.$vue_app_name = window.GENIEMODEL = app.mount(rootSelector); - } // end of initStipple - - " - - , - - " - - function initWatchers(){ - " - - , - join( - [Stipple.watch(string("window.", vue_app_name), field, Stipple.channel_js_name, debounce, model) for field in fieldnames(Stipple.get_concrete_type(M)) - if Stipple.has_frontend_watcher(field, model)] - ) - , - - " - } // end of initWatchers - - " - - , - """ - - window.parse_payload = function(payload){ - if (payload.key) { - window.$(vue_app_name).updateField(payload.key, payload.value); + window.parse_payload = function(WebChannel, payload){ + if (payload.key) { + WebChannel.parent.updateField(payload.key, payload.value); + } } - } - - function app_ready() { - $vue_app_name.channel_ = window.CHANNEL; - $vue_app_name.isready = true; - Genie.Revivers.addReviver(window.$(vue_app_name).revive_jsfunction); - $(transport == Genie.WebChannels && - " + + function app_ready(app) { + if (app.WebChannel == Genie.AllWebChannels[0]) Genie.Revivers.addReviver(app.revive_jsfunction); + app.isready = true; + """, + transport == Genie.WebChannels && + """ try { if (Genie.Settings.webchannels_keepalive_frequency > 0) { - clearInterval($vue_app_name.keepalive_interval); - $vue_app_name.keepalive_interval = setInterval(keepalive, Genie.Settings.webchannels_keepalive_frequency); + keepaliveTimer(app.WebChannel, 0); } } catch (e) { if (Genie.Settings.env === 'dev') { console.error('Error setting WebSocket keepalive interval: ' + e); } } - ") - + """, + """ if (Genie.Settings.env === 'dev') { console.info('App starting'); } - }; + }; - if ( window.autorun === undefined || window.autorun === true ) { - initStipple('#$vue_app_name'); - initWatchers(); + function initStipple$vue_app_name(appName, rootSelector, channel){ + // components = Stipple.init($( core_theme ? "{theme: '$theme'}" : "" )); + const app = Vue.createApp($( replace(vue_app, "'$(Stipple.UNDEFINED_PLACEHOLDER)'"=>Stipple.UNDEFINED_VALUE) )) + /* Object.entries(components).forEach(([key, value]) => { + app.component(key, value) + }); */ + Stipple.init( app, $( core_theme ? "{theme: '$theme'}" : "" )); + $globalcomps + $comps + // gather legacy global options + app.prototype = {} + $(plugins(M)) + // apply legacy global options + Object.entries(app.prototype).forEach(([key, value]) => { + app.config.globalProperties[key] = value + }); + + const stippleApp = window[appName] = window.GENIEMODEL = app.mount(rootSelector); + stippleApp.WebChannel = Genie.initWebChannel(channel); + stippleApp.WebChannel.parent = stippleApp; + stippleApp.channel_ = channel; + + return stippleApp; + } // end of initStipple + + function initWatchers$vue_app_name(app){ + """, + join( + [Stipple.watch("app", field, Stipple.channel_js_name, debounce, model) for field in fieldnames(Stipple.get_concrete_type(M)) + if Stipple.has_frontend_watcher(field, model)] + ), + + """ + } // end of initWatchers + + function create$vue_app_name(channel) { + window.counter$vue_app_name = window.counter$vue_app_name || 1 + const appName = '$vue_app_name' + ((counter$vue_app_name == 1) ? '' : '_' + window.counter$vue_app_name) + const rootSelector = '#$vue_app_name' + ((counter$vue_app_name == 1) ? '' : '-' + window.counter$vue_app_name) + counter$vue_app_name++ + + if ( window.autorun === undefined || window.autorun === true ) { + app = initStipple$vue_app_name(appName, rootSelector, channel); + initWatchers$vue_app_name(app); + + app.WebChannel.subscriptionHandlers.push(function(event) { + app_ready(app); + }); + } + } - Genie.WebChannels.subscriptionHandlers.push(function(event) { - app_ready(); - }); - } + // create$vue_app_name() + // is called via script with addEventListener to support multiple apps """ ) diff --git a/src/Layout.jl b/src/Layout.jl index 86be5b9a..4f02fc77 100644 --- a/src/Layout.jl +++ b/src/Layout.jl @@ -16,6 +16,25 @@ const THEMES = Ref(Function[]) const FLEXGRID_KWARGS = [:col, :xs, :sm, :md, :lg, :xl, :gutter, :xgutter, :ygutter] +""" + make_unique!(src::Vector, condition::Union{Nothing, Function} = nothing) + +Utility function for removing duplicates from a vector that fulfill a given condition. +""" +function make_unique!(src::Vector, condition::Union{Nothing, Function} = nothing) + seen = Int[] + dups = Int[] + for (i, name) in enumerate(src) + if name ∈ view(src, seen) && (condition === nothing || condition(name)) + push!(dups, i) + else + push!(seen, i) + end + end + + deleteat!(src, dups) +end + """ function layout(output::Union{String,Vector}; partial::Bool = false, title::String = "", class::String = "", style::String = "", head_content::String = "", channel::String = Genie.config.webchannels_default_route) :: String @@ -41,19 +60,22 @@ julia> layout([ "Hello\n" ``` """ -function layout(output::Union{S,Vector}, m::M; +function layout(output::Union{S,Vector}, m::Union{M, Vector{M}}; partial::Bool = false, title::String = "", class::String = "", style::String = "", head_content::Union{AbstractString, Vector{<:AbstractString}} = "", channel::String = Stipple.channel_js_name, core_theme::Bool = true)::ParsedHTMLString where {M<:ReactiveModel, S<:AbstractString} isa(output, Vector) && (output = join(output, '\n')) + m isa Vector || (m = [m]) content = [ output theme(; core_theme) - Stipple.deps(m) + Stipple.deps.(m)... ] + make_unique!(content, contains(r"src=|href="i)) + partial && return content Genie.Renderer.Html.doc( @@ -84,21 +106,34 @@ julia> page(:elemid, [ "\n
Hello
\n" ``` """ -function page(model::M, args...; +function page(model::Union{M, Vector{M}}, args...; + pagetemplate = (x...) -> join([x...], '\n'), partial::Bool = false, title::String = "", class::String = "container", style::String = "", channel::String = Genie.config.webchannels_default_route, head_content::Union{AbstractString, Vector{<:AbstractString}} = "", prepend::Union{S,Vector} = "", append::Union{T,Vector} = [], core_theme::Bool = true, kwargs...)::ParsedHTMLString where {M<:Stipple.ReactiveModel, S<:AbstractString,T<:AbstractString} + model isa Vector || (model = [model]) + uis = if !isempty(args) + args[1] isa Vector ? args[1] : [args[1]] + else + "" + end + counter = Dict{DataType, Int}() + + function rootselector(m::M) where M <:ReactiveModel + AM = Stipple.get_abstract_type(M) + counter[AM] = get(counter, AM, 0) + 1 + return (counter[AM] == 1) ? vm(m) : "$(vm(m))-$(counter[AM])" + end layout( [ join(prepend) - Genie.Renderer.Html.div(id = vm(M), args...; class = class, kwargs...) + pagetemplate([Genie.Renderer.Html.div(id = rootselector(m), ui, args[2:end]...; class = class, kwargs...) for (m, ui) in zip(model, uis)]...) join(append) ], model; - partial = partial, title = title, style = style, head_content = head_content, channel = channel, - core_theme = core_theme) + partial, title, style, head_content, channel, core_theme) end const app = page diff --git a/src/Pages.jl b/src/Pages.jl index 368a0f6c..6351d925 100644 --- a/src/Pages.jl +++ b/src/Pages.jl @@ -42,9 +42,11 @@ function Page( route::Union{Route,String}; Core.eval(context, model) elseif isa(model, Module) context = model - () -> @eval(context, Stipple.ReactiveTools.@init(debounce = $debounce, transport = $transport, core_theme = $core_theme)) + () -> Stipple.ReactiveTools.init_model(context; debounce, transport, core_theme) elseif model isa DataType - () -> @eval(context, Stipple.ReactiveTools.@init($model; debounce = $debounce, transport = $transport, core_theme = $core_theme)) + # as model is being redefined, we need to create a copy + mymodel = model + () -> Stipple.ReactiveTools.init_model(mymodel; debounce, transport, core_theme) else model end @@ -67,7 +69,7 @@ function Page( route::Union{Route,String}; route.action = () -> (isa(view, Function) ? html! : html)(view; layout, context, model = (isa(model, Function) ? Base.invokelatest(model) : model), kwargs...) - page = Page(route, view, typeof((isa(model, Function) || isa(model, DataType) ? Base.invokelatest(model) : model)), layout, context) + page = Page(route, view, model, layout, context) if isempty(_pages) push!(_pages, page) diff --git a/src/ReactiveTools.jl b/src/ReactiveTools.jl index 33503752..400d1924 100644 --- a/src/ReactiveTools.jl +++ b/src/ReactiveTools.jl @@ -5,10 +5,7 @@ using MacroTools using MacroTools: postwalk using OrderedCollections import Genie -import Stipple: deletemode!, parse_expression!, init_storage - -# definition of variables -export @readonly, @private, @in, @out, @jsfn, @readonly!, @private!, @in!, @out!, @jsfn!, @mixin +import Stipple: deletemode!, parse_expression!, parse_expression, init_storage, striplines, striplines! #definition of handlers/events export @onchange, @onbutton, @event, @notify @@ -33,12 +30,6 @@ export @before_create, @created, @before_mount, @mounted, @before_update, @updat export DEFAULT_LAYOUT, Page -export @onchangeany # deprecated - -const REACTIVE_STORAGE = LittleDict{Module,LittleDict{Symbol,Expr}}() -const HANDLERS = LittleDict{Module,Vector{Expr}}() -const TYPES = LittleDict{Module,Union{<:DataType,Nothing}}() - const HANDLERS_FUNCTIONS = LittleDict{Type{<:ReactiveModel},Function}() function DEFAULT_LAYOUT(; title::String = "Genie App", @@ -106,21 +97,20 @@ function DEFAULT_LAYOUT(; title::String = "Genie App", end function model_typename(m::Module) - isdefined(m, :__typename__) ? m.__typename__[] : "$(m)_ReactiveModel" + isdefined(m, :__typename__) ? m.__typename__[] : Symbol("$(m)_ReactiveModel") end macro appname(expr) expr isa Symbol || (expr = Symbol(@eval(__module__, $expr))) - clear_type(__module__) ex = quote end if isdefined(__module__, expr) push!(ex.args, :(Stipple.ReactiveTools.delete_handlers_fn($__module__))) push!(ex.args, :(Stipple.ReactiveTools.delete_events($expr))) end - if isdefined(__module__, :__typename__) && __module__.__typename__ isa Ref{String} - push!(ex.args, :(__typename__[] = $(string(expr)))) + if isdefined(__module__, :__typename__) && __module__.__typename__ isa Ref{Symbol} + push!(ex.args, :(__typename__[] = Symbol($(string(expr))))) else - push!(ex.args, :(const __typename__ = Ref{String}($(string(expr))))) + push!(ex.args, :(const __typename__ = Ref{Symbol}(Symbol($(string(expr)))))) push!(ex.args, :(__typename__[])) end :($ex) |> esc @@ -132,13 +122,6 @@ macro appname() :(isdefined($__module__, :__typename__) ? @appname($appname) : $appname) |> esc end -function Stipple.init_storage(m::Module) - (m == @__MODULE__) && return nothing - haskey(REACTIVE_STORAGE, m) || (REACTIVE_STORAGE[m] = Stipple.init_storage()) - haskey(TYPES, m) || (TYPES[m] = nothing) - REACTIVE_STORAGE[m] -end - function Stipple.setmode!(expr::Expr, mode::Int, fieldnames::Symbol...) fieldname in [Stipple.CHANNELFIELDNAME, :modes__] && return expr.args[2] isa Expr && expr.args[2].args[1] == :(Stipple._deepcopy) && (expr.args[2] = expr.args[2].args[2]) @@ -164,21 +147,6 @@ end #===# -function clear_type(m::Module) - TYPES[m] = nothing -end - -function delete_bindings!(m::Module) - clear_type(m) - delete!(REACTIVE_STORAGE, m) - nothing -end - -function bindings(m) - init_storage(m) - REACTIVE_STORAGE[m] -end - function delete_handlers_fn(m::Module) if isdefined(m, :__GF_AUTO_HANDLERS__) Base.delete_method.(methods(m.__GF_AUTO_HANDLERS__)) @@ -186,7 +154,9 @@ function delete_handlers_fn(m::Module) end function delete_events(m::Module) - haskey(TYPES, m) && TYPES[m] isa DataType && delete_events(TYPES[m]) + modelname = model_typename(m) + M = @eval m $modelname + delete_events(M) end function delete_events(::Type{M}) where M @@ -203,7 +173,6 @@ function delete_events(::Type{M}) where M end function delete_handlers!(m::Module) - delete!(HANDLERS, m) delete_handlers_fn(m) delete_events(m) nothing @@ -219,34 +188,9 @@ end Deletes all reactive variables and code in a model. """ macro clear() - delete_bindings!(__module__) delete_handlers!(__module__) end -macro clear(args...) - haskey(REACTIVE_STORAGE, __module__) || return - for arg in args - arg in [Stipple.CHANNELFIELDNAME, :modes__] && continue - delete!(REACTIVE_STORAGE[__module__], arg) - end - deletemode!(REACTIVE_STORAGE[__module__][:modes__], args...) - - update_storage(__module__) - - REACTIVE_STORAGE[__module__] -end - -""" -```julia -@clear_vars -``` - -Deletes all reactive variables in a model. -""" -macro clear_vars() - delete_bindings!(__module__) -end - """ ```julia @clear_handlers @@ -260,16 +204,8 @@ end import Stipple.@type macro type() - Stipple.init_storage(__module__) - type = if TYPES[__module__] !== nothing - TYPES[__module__] - else - modelname = Symbol(model_typename(__module__)) - storage = REACTIVE_STORAGE[__module__] - TYPES[__module__] = @eval(__module__, Stipple.@type($modelname, $storage)) - end - - esc(:($type)) + modelname = model_typename(__module__) + esc(:($modelname)) end import Stipple.@clear_cache @@ -418,31 +354,14 @@ macro clear_debounce() :(Stipple.debounce(Stipple.@type(), nothing)) |> esc end -function update_storage(m::Module) - clear_type(m) - # isempty(Stipple.Pages._pages) && return - # instance = @eval m Stipple.@type() - # for p in Stipple.Pages._pages - # p.context == m && (p.model = instance) - # end -end - -import Stipple: @vars, @add_vars +import Stipple: @vars macro vars(expr) - init_storage(__module__) - - REACTIVE_STORAGE[__module__] = @eval(__module__, Stipple.@var_storage($expr)) - - update_storage(__module__) - REACTIVE_STORAGE[__module__] -end - -macro add_vars(expr) - init_storage(__module__) - REACTIVE_STORAGE[__module__] = Stipple.merge_storage(REACTIVE_STORAGE[__module__], @eval(__module__, Stipple.@var_storage($expr)); context = __module__) - - update_storage(__module__) + modelname = model_typename(__module__) + storage = Stipple.init_storage() + quote + Stipple.ReactiveTools.@vars $modelname $expr + end |> esc end macro model() @@ -474,72 +393,17 @@ The code block passed to @app implements the app's logic, handling the states of end ``` """ -macro app(expr) - delete_bindings!(__module__) - delete_handlers!(__module__) - - init_handlers(__module__) - init_storage(__module__) - +macro app(expr = Expr(:block)) + modelname = model_typename(__module__) + storage = Stipple.init_storage() quote - $expr - - Stipple.ReactiveTools.@handlers + Stipple.ReactiveTools.@app $modelname $expr __GF_AUTO_HANDLERS__ + $modelname end |> esc end #===# -function binding(expr::Symbol, m::Module, @nospecialize(mode::Any = nothing); source = nothing, reactive = true) - binding(:($expr = $expr), m, mode; source, reactive) -end - -function binding(expr::Expr, m::Module, @nospecialize(mode::Any = nothing); source = nothing, reactive = true) - (m == @__MODULE__) && return nothing - - intmode = mode isa Integer ? Int(mode) : @eval Stipple.$mode - init_storage(m) - - var, field_expr = parse_expression!(expr, reactive ? mode : nothing, source, m) - REACTIVE_STORAGE[m][var] = field_expr - - reactive || setmode!(REACTIVE_STORAGE[m][:modes__], intmode, var) - reactive && setmode!(REACTIVE_STORAGE[m][:modes__], PUBLIC, var) - - # remove cached type and instance, update pages - update_storage(m) -end - -function binding(expr::Expr, storage::LittleDict{Symbol, Expr}, @nospecialize(mode::Any = nothing); source = nothing, - reactive = true, m::Module) - intmode = mode isa Integer ? Int(mode) : @eval Stipple.$mode - - var, field_expr = parse_expression!(expr, reactive ? mode : nothing, source, m) - storage[var] = field_expr - - reactive || setmode!(storage[:modes__], intmode, var) - reactive && setmode!(storage[:modes__], PUBLIC, var) - - storage -end - -# this macro needs to run in a macro where `expr`is already defined -macro report_val() - quote - val = expr isa Symbol ? expr : expr.args[2] - issymbol = val isa Symbol - :(if $issymbol - if isdefined(@__MODULE__, $(QuoteNode(val))) - $val - else - @info(string("Warning: Variable '", $(QuoteNode(val)), "' not yet defined")) - end - else - Stipple.Observables.to_value($val) - end) |> esc - end |> esc -end - # this macro needs to run in a macro where `expr`is already defined macro define_var() quote @@ -550,144 +414,56 @@ macro define_var() end |> esc end -# works with -# @in a = 2 -# @in a::Vector = [1, 2, 3] -# @in a::Vector{Int} = [1, 2, 3] - -# the @in, @out and @private macros below are defined so a docstring can be attached -# the actual macro definition is done in the for loop further down -""" -```julia -@in(expr) -``` - -Declares a reactive variable that is public and can be written to from the UI. - -**Usage** -```julia -@app begin - @in N = 0 -end -``` -""" -macro in end - -""" -```julia -@out(expr) -``` - -Declares a reactive variable that is public and readonly. - -**Usage** -```julia -@app begin - @out N = 0 -end -``` -""" -macro out end - -""" -```julia -@private(expr) -``` - -Declares a non-reactive variable that cannot be accessed by UI code. - -**Usage** -```julia -@app begin - @private N = 0 -end -``` -""" -macro private end - -for (fn, mode) in [(:in, :PUBLIC), (:out, :READONLY), (:jsnfn, :JSFUNCTION), (:private, :PRIVATE)] - fn! = Symbol(fn, "!") - Core.eval(@__MODULE__, quote - - macro $fn!(expr) - binding(expr isa Symbol ? expr : copy(expr), __module__, $mode; source = __source__) - esc(:($expr)) - end - - macro $fn!(flag, expr) - flag != :non_reactive && return esc(:(ReactiveTools.$fn!($flag, _, $expr))) - binding(expr isa Symbol ? expr : copy(expr), __module__, $mode; source = __source__, reactive = false) - esc(:($expr)) - end - - macro $fn(location, flag, expr) - reactive = flag != :non_reactive - ex = [expr isa Symbol ? expr : copy(expr)] - loc = location isa Symbol ? QuoteNode(location) : location - - quote - local location = isdefined($__module__, $loc) ? eval($loc) : $loc - local storage = location isa DataType ? Stipple.model_to_storage(location) : location isa LittleDict ? location : Stipple.init_storage() - - Stipple.ReactiveTools.binding($ex[1], storage, $$mode; source = $__source__, reactive = $reactive, m = $__module__) - location isa DataType || location isa Symbol ? eval(:(Stipple.@type($$loc, $storage))) : location - end |> esc - end - - macro $fn(expr) - binding(expr isa Symbol ? expr : copy(expr), __module__, $mode; source = __source__) - @report_val() - end - - macro $fn(flag, expr) - flag != :non_reactive && return esc(:(ReactiveTools.@fn($flag, _, $expr))) - binding(expr isa Symbol ? expr : copy(expr), __module__, $mode; source = __source__, reactive = false) - @report_val() - end - end) -end - -macro mixin(expr, prefix = "", postfix = "") - # if prefix is not a String then call the @mixin version for generic model types - prefix isa String || return quote - @mixin $expr $prefix $postfix "" +function parse_mixin_params(params) + striplines!(params) + mixin, prefix, postfix = if length(params) == 1 && params[1] isa Expr && hasproperty(params[1], :head) && params[1].head == :(::) + params[1].args[2], string(params[1].args[1]), "" + elseif length(params) == 1 + params[1], "", "" + elseif length(params) == 2 + params[1], string(params[2]), "" + elseif length(params) == 3 + params[1], string(params[2]), string(params[3]) + else + error("1, 2, or 3 arguments expected, found $(length(params))") end + mixin, prefix, postfix +end + +function parse_macros(expr::Expr, storage::LittleDict, m::Module, let_block::Expr = nothing, vars::Set = Set()) + expr.head == :macrocall || return expr + flag = :nothing + fn = Symbol(String(expr.args[1])[2:end]) + mode = Dict(:in => PUBLIC, :out => READONLY, :jsnfn => JSFUNCTION, :private => PRIVATE, :mixin => 0)[fn] + + source = filter(x -> x isa LineNumberNode, expr.args) + source = isempty(source) ? "" : last(source) + striplines!(expr) + params = expr.args[2:end] + + if fn != :mixin + if length(params) == 1 + expr = params + elseif length(params) == 2 + flag, expr = params + else + error("1 or 2 arguments expected, found $(length(params))") + end - storage = init_storage(__module__) - - Stipple.ReactiveTools.update_storage(__module__) - Core.eval(__module__, quote - Stipple.ReactiveTools.@mixin $storage $expr $prefix $postfix - end) - quote end -end - -macro mixin(location, expr, prefix, postfix) - if hasproperty(expr, :head) && expr.head == :(::) - prefix = string(expr.args[1]) - expr = expr.args[2] + reactive = flag != :non_reactive + var, ex = parse_expression(expr[1], mode, source, m, let_block, vars) + storage[var] = ex + elseif fn == :mixin + mixin, prefix, postfix = parse_mixin_params(params) + mixin_storage = Stipple.model_to_storage(@eval(m, $mixin), prefix, postfix) + merge!(storage, Stipple.merge_storage(storage, mixin_storage; context = m)) + else + error("Unknown macro @$fn") end - loc = location isa Symbol ? QuoteNode(location) : location - - x = Core.eval(__module__, expr) - quote - local location = $loc isa Symbol && isdefined($__module__, $loc) ? $__module__.$(loc isa QuoteNode ? loc.value : loc) : $loc - local storage = location isa DataType ? Stipple.model_to_storage(location) : location isa LittleDict ? location : Stipple.init_storage() - M = $x isa DataType ? $x : typeof($x) # really needed? - local mixin_storage = Stipple.model_to_storage(M, $(QuoteNode(prefix)), $postfix) - - merge!(storage, Stipple.merge_storage(storage, mixin_storage; context = @__MODULE__)) - location isa DataType || location isa Symbol ? eval(:(Stipple.@type($$loc, $storage))) : location - mixin_storage - end |> esc end #===# -function init_handlers(m::Module) - get!(Vector{Expr}, HANDLERS, m) -end - """ @init(kwargs...) @@ -729,92 +505,95 @@ macro init(args...) called_without_type = isnothing(type_pos) if called_without_type - type_pos = 0 # to prevent erroring in definition of 'handlersfn' - insert!(init_args, Stipple.has_parameters(init_args) ? 2 : 1, :(Stipple.@type())) + typename = model_typename(__module__) + insert!(init_args, Stipple.has_parameters(init_args) ? 2 : 1, typename) + else + typename = init_args[type_pos] end quote - local new_handlers = false + Stipple.ReactiveTools.init_model($(init_args...)) + end |> esc +end - local initfn = - if isdefined($__module__, :init_from_storage) && Stipple.USE_MODEL_STORAGE[] - $__module__.init_from_storage - else - Stipple.init - end +function init_model(M::Type{<:ReactiveModel}, args...; context = nothing, kwargs...) + m = parentmodule(M) + initfn = if isdefined(m, :init_from_storage) && Stipple.USE_MODEL_STORAGE[] + m.init_from_storage + else + Stipple.init + end + handlersfn = if context !== nothing && isdefined(M, :__GF_AUTO_HANDLERS__) + M.__GF_AUTO_HANDLERS__ + else + Stipple.ReactiveTools.HANDLERS_FUNCTIONS[M] + end - local handlersfn = - if !$called_without_type - # writing '$(init_kwargs[type_pos])' generates an error during a pre-evaluation - # possibly from Revise? - # we use 'get' instead of 'getindex' - Stipple.ReactiveTools.HANDLERS_FUNCTIONS[$(get(init_args, type_pos, "dummy"))] - else - if isdefined($__module__, :__GF_AUTO_HANDLERS__) - if length(methods($__module__.__GF_AUTO_HANDLERS__)) == 0 - @eval(@handlers()) - new_handlers = true - end - $__module__.__GF_AUTO_HANDLERS__ - else - identity - end - end + model = initfn(M, args...; kwargs...) |> handlersfn - instance = let model = initfn($(init_args...)) - new_handlers ? Base.invokelatest(handlersfn, model) : handlersfn(model) - end - for p in Stipple.Pages._pages - p.context == $__module__ && (p.model = instance) - end + # Update the model in all pages where it has been set as instance of an app. + # Where it has been set as ReactiveModel type, no change is required + for p in Stipple.Pages._pages + p.context == m && p.model isa M && (p.model = model) + end + model +end - instance - end |> esc +function init_model(m::Module, args...; kwargs...) + init_model(@eval(m, Stipple.@type), args...; context = m, kwargs...) end macro app(typename, expr, handlers_fn_name = :handlers) - # indicate to the @handlers macro that old typefields have to be cleared - # (avoids model_to_storage) - newtypename = Symbol(typename, "_!_") quote - let model = Stipple.ReactiveTools.@handlers $newtypename $expr $handlers_fn_name - Stipple.ReactiveTools.HANDLERS_FUNCTIONS[$typename] = $handlers_fn_name - model - end + Stipple.ReactiveTools.@handlers $typename $expr $handlers_fn_name + Stipple.ReactiveTools.HANDLERS_FUNCTIONS[$typename] = $handlers_fn_name + $typename, $handlers_fn_name end |> esc end macro handlers() - handlers = init_handlers(__module__) - + modelname = model_typename(__module__) + empty_block = Expr(:block) quote - function __GF_AUTO_HANDLERS__(__model__) - $(handlers...) - - return __model__ - end + Stipple.ReactiveTools.@handlers $modelname $empty_block __GF_AUTO_HANDLERS__ end |> esc end macro handlers(expr) - delete_handlers!(__module__) - init_handlers(__module__) - + modelname = model_typename(__module__) quote - $expr - - @handlers + Stipple.ReactiveTools.@handlers $modelname $expr __GF_AUTO_HANDLERS__ end |> esc end -macro handlers(typename, expr, handlers_fn_name = :handlers) - newtype = endswith(String(typename), "_!_") - newtype && (typename = Symbol(String(typename)[1:end-3])) +""" + get_varnames(app_expr::Vector, context::Module) +Return a list of all non-internal variable names used in a vector of var definitions lines. +""" +function get_varnames(app_expr::Vector, context::Module) + varnames = copy(Stipple.AUTOFIELDS) + for ex in app_expr + ex isa LineNumberNode && continue + if ex.args[1] ∈ [Symbol("@in"), Symbol("@out"), Symbol("@jsfunction"), Symbol("@private")] + res = Stipple.get_varname(ex) + push!(varnames, res isa Symbol ? res : res[1]) + elseif ex.args[1] == Symbol("@mixin") + mixin, prefix, postfix = parse_mixin_params(ex.args[2:end]) + fnames = setdiff(@eval(context, collect($mixin isa LittleDict ? keys($mixin) : propertynames($mixin()))), Stipple.AUTOFIELDS, Stipple.INTERNALFIELDS) + prefix === nothing || (fnames = Symbol.(prefix, fnames, postfix)) + append!(varnames, fnames) + end + end + @assert(length(unique(varnames)) == length(varnames), "Duplicate field names detected") + varnames +end + +macro handlers(typename, expr, handlers_fn_name = :handlers) expr = wrap(expr, :block) i_start = 1 handlercode = [] - initcode = quote end + initcode = [] for (i, ex) in enumerate(expr.args) if ex isa Expr @@ -822,68 +601,57 @@ macro handlers(typename, expr, handlers_fn_name = :handlers) ex_index = .! isa.(ex.args, LineNumberNode) if sum(ex_index) < 4 pos = findall(ex_index)[2] - insert!(ex.args, pos, typename) + insert!(ex.args, pos, :__storage__) end push!(handlercode, expr.args[i_start:i]...) + elseif ex.head == :macrocall && ex.args[1] in Symbol.(["@in", "@out", "@private", "@readonly", "@jsfn", "@mixin"]) + push!(initcode, expr.args[i_start:i]...) else - if ex.head == :macrocall && ex.args[1] in Symbol.(["@in", "@out", "@private", "@readonly", "@jsfn", "@mixin"]) - ex_index = isa.(ex.args, Union{Symbol, Expr}) - pos = findall(ex_index)[2] - sum(ex_index) == 2 && ex.args[1] != Symbol("@mixin") && insert!(ex.args, pos, :_) - insert!(ex.args, pos, :__storage__) - end - push!(initcode.args, expr.args[i_start:i]...) + println("Warning: Unrecognized macro in handlers: ", ex) + push!(handlercode, ex) end i_start = i + 1 end end - # model_to_storage is only needed when we add variables to an existing type. - no_new_vars = findfirst(x -> x isa Expr, initcode.args) === nothing - # if we redefine a type newtype is true - if isdefined(__module__, typename) && no_new_vars && ! newtype - # model is already defined and no variables are added and we are not redefining a model type - else - # we need to define a type ... - storage = if ! newtype && isdefined(__module__, typename) && ! no_new_vars - @eval(__module__, Stipple.model_to_storage($typename)) - else - Stipple.init_storage() - end - initcode = quote - # define a local variable __storage__ with the value of storage - # that will be used by the macro afterwards - __storage__ = $storage - # add more definitions to __storage___ - $(initcode.args...) - end + storage = Stipple.init_storage() + varnames = get_varnames(initcode, __module__) - # needs to be executed before evaluation of handler code - # because the handler code depends on the model fields. - @eval __module__ begin - # execution of initcode will fill up the __storage__ - $initcode - Stipple.@type($typename, values(__storage__)) - end - end + filter!(x -> !isa(x, LineNumberNode), initcode) + let_block = Expr(:block, :(_ = 0)) + required_vars = Set() + Stipple.required_evals!.(initcode, Ref(required_vars)) + parse_macros.(initcode, Ref(storage), Ref(__module__), Ref(let_block), Ref(required_vars)) + # if no initcode is provided and typename is already defined, don't overwrite the existing type and just declare the handlers function + initcode_final = isempty(initcode) && isdefined(__module__, typename) ? Expr(:block) : :(Stipple.@type($typename, $storage)) handlercode_final = [] + d = LittleDict(varnames .=> varnames) + d_expr = :($d) for ex in handlercode if ex isa Expr + replace!(ex.args, :__storage__ => d_expr) push!(handlercode_final, @eval(__module__, $ex)) else push!(handlercode_final, ex) end end + + # println("initcode: ", initcode) + # println("initcode_final: ", initcode_final) + # println("handlercode: ", handlercode) + # println("handlercode_final: ", handlercode_final) quote + $(initcode_final) Stipple.ReactiveTools.delete_events($typename) - + function $handlers_fn_name(__model__) $(handlercode_final...) __model__ end + Stipple.ReactiveTools.HANDLERS_FUNCTIONS[$typename] = $handlers_fn_name ($typename, $handlers_fn_name) end |> esc end @@ -992,12 +760,16 @@ function fieldnames_to_fieldcontent(expr, vars, replace_vars) end function get_known_vars(M::Module) - init_storage(M) + modeltype = @eval M Stipple.@type + get_known_vars(modeltype) +end + +function get_known_vars(storage::LittleDict) reactive_vars = Symbol[] non_reactive_vars = Symbol[] - for (k, v) in REACTIVE_STORAGE[M] + for (k, v) in storage k in Stipple.INTERNALFIELDS && continue - is_reactive = startswith(string(Stipple.split_expr(v)[2]), r"(Stipple\.)?R(eactive)?($|{)") + is_reactive = v isa Symbol ? true : startswith(string(Stipple.split_expr(v)[2]), r"(Stipple\.)?R(eactive)?($|{)") push!(is_reactive ? reactive_vars : non_reactive_vars, k) end reactive_vars, non_reactive_vars @@ -1045,43 +817,25 @@ macro onchange(var, expr) end macro onchange(location, vars, expr) - loc::Union{Module, Type{<:M}} where M<:ReactiveModel = @eval __module__ $location + loc::Union{Module, Type{<:ReactiveModel}, LittleDict} = @eval __module__ $location vars = wrap(vars, :tuple) expr = wrap(expr, :block) - loc isa Module && init_handlers(loc) known_reactive_vars, known_non_reactive_vars = get_known_vars(loc) known_vars = vcat(known_reactive_vars, known_non_reactive_vars) on_vars = fieldnames_to_fields(vars, known_vars) expr, used_vars = mask(expr, known_vars) - do_vars = Symbol[] - - for a in vars.args - push!(do_vars, a isa Symbol && ! in(a, used_vars) ? a : :_) - end - replace_reactive_vars = setdiff(known_reactive_vars, do_vars) - replace_non_reactive_vars = setdiff(known_non_reactive_vars, do_vars) - - expr = fieldnames_to_fields(expr, known_non_reactive_vars, replace_non_reactive_vars) - expr = fieldnames_to_fieldcontent(expr, known_reactive_vars, replace_reactive_vars) - expr = unmask(expr, vcat(replace_reactive_vars, replace_non_reactive_vars)) + expr = fieldnames_to_fields(expr, known_non_reactive_vars) + expr = fieldnames_to_fieldcontent(expr, known_reactive_vars) + expr = unmask(expr, vcat(known_reactive_vars, known_non_reactive_vars)) fn = length(vars.args) == 1 ? :on : :onany - ex = quote - $fn($(on_vars.args...)) do $(do_vars...) + :($fn($(on_vars.args...)) do _... $(expr.args...) end - end - - loc isa Module && push!(HANDLERS[__module__], ex) - output = [ex] - quote - function __GF_AUTO_HANDLERS__ end - Base.delete_method.(methods(__GF_AUTO_HANDLERS__)) - $output[end] - end |> esc + ) |> QuoteNode end macro onchangeany(var, expr) @@ -1122,9 +876,8 @@ macro onbutton(var, expr) end macro onbutton(location, var, expr) - loc::Union{Module, Type{<:ReactiveModel}} = @eval __module__ $location + loc::Union{Module, Type{<:ReactiveModel}, LittleDict} = @eval __module__ $location expr = wrap(expr, :block) - loc isa Module && init_handlers(loc) known_reactive_vars, known_non_reactive_vars = get_known_vars(loc) known_vars = vcat(known_reactive_vars, known_non_reactive_vars) @@ -1136,20 +889,9 @@ macro onbutton(location, var, expr) expr = fieldnames_to_fieldcontent(expr, known_reactive_vars) expr = unmask(expr, known_vars) - ex = :(onbutton($var) do + :(onbutton($var) do $(expr.args...) - end) - - output = quote end - - if loc isa Module - push!(HANDLERS[__module__], ex) - push!(output.args, :(function __GF_AUTO_HANDLERS__ end)) - push!(output.args, :(Base.delete_method.(methods(__GF_AUTO_HANDLERS__)))) - end - push!(output.args, QuoteNode(ex)) - - output |> esc + end) |> QuoteNode end #===# @@ -1181,25 +923,21 @@ macro page(expressions...) defaults = Dict( :layout => Stipple.ReactiveTools.DEFAULT_LAYOUT(), :context => __module__, - :model => nothing + :model => __module__ ) ) model_parent, model_ind, model_expr = Stipple.locate_kwarg(args, :model) model = @eval(__module__, $model_expr) - if model === nothing || model isa DataType && model <: ReactiveModel - # remove all other kwargs that are not meant for `@init` - init_kwargs = Stipple.delete_kwargs(args, [:layout, :model, :context]) - - # add the type if model is a modeltype - if model !== nothing - insert!(init_kwargs, Stipple.has_parameters(init_kwargs) ? 2 : 1, model_expr) + if model isa Module + # the next lines are added for backward compatibility + # if the app is programmed according to the latest API, + # eval will not be called; will e removed in the future + typename = model_typename(__module__) + if !isdefined(__module__, typename) + @warn "App not yet defined, this is strongly discouraged, please define an app first" + @eval(__module__, @app) end - - # modify the entry of the :model keyword by an init function with respective init_kwargs - model_parent[model_ind] = :($(Expr(:kw, :model, () -> @eval(__module__, Stipple.ReactiveTools.@init($(init_kwargs...)))))) - else - nothing end :(Stipple.Pages.Page($(args...), $url, view = $view)) |> esc diff --git a/src/Stipple.jl b/src/Stipple.jl index 53b1e9cb..b9697b23 100644 --- a/src/Stipple.jl +++ b/src/Stipple.jl @@ -20,6 +20,8 @@ const PRECOMPILE = Ref(false) const ALWAYS_REGISTER_CHANNELS = Ref(true) const USE_MODEL_STORAGE = Ref(true) +import MacroTools + """ Disables the automatic storage and retrieval of the models in the session. Useful for large models. @@ -113,7 +115,7 @@ using .NamedTuples export JSONParser, JSONText, json, @json, jsfunction, @jsfunction_str const config = Genie.config -const channel_js_name = "window.CHANNEL" +const channel_js_name = "'not_assigned'" const OptDict = OrderedDict{Symbol, Any} opts(;kwargs...) = OptDict(kwargs...) @@ -342,10 +344,10 @@ changed on the frontend, it is pushed over to the backend using `channel`, at a function watch(vue_app_name::String, fieldname::Symbol, channel::String, debounce::Int, model::M; jsfunction::String = "")::String where {M<:ReactiveModel} js_channel = isempty(channel) ? "window.Genie.Settings.webchannels_default_route" : - (channel == Stipple.channel_js_name ? Stipple.channel_js_name : "'$channel'") + "$vue_app_name.channel_" isempty(jsfunction) && - (jsfunction = "Genie.WebChannels.sendMessageTo($js_channel, 'watchers', {'payload': {'field':'$fieldname', 'newval': newVal, 'oldval': oldVal, 'sesstoken': document.querySelector(\"meta[name='sesstoken']\")?.getAttribute('content')}});") + (jsfunction = "$vue_app_name.push('$fieldname')") output = IOBuffer() if fieldname == :isready @@ -812,11 +814,18 @@ function injectdeps(output::Vector{AbstractString}, M::Type{<:ReactiveModel}) :: output end - +# no longer needed, replaced by initscript function channelscript(channel::String) :: String - Genie.Renderer.Html.script(["window.CHANNEL = '$(channel)';"]) + Genie.Renderer.Html.script([""" + document.addEventListener('DOMContentLoaded', () => window.Genie.initWebChannel('$channel') ); + """]) end +function initscript(vue_app_name, channel) :: String + Genie.Renderer.Html.script([""" + document.addEventListener('DOMContentLoaded', () => window.create$vue_app_name('$channel') ); + """]) +end """ function deps(channel::String = Genie.config.webchannels_default_route) @@ -826,7 +835,7 @@ Outputs the HTML code necessary for injecting the dependencies in the page (the function deps(m::M) :: Vector{String} where {M<:ReactiveModel} channel = getchannel(m) output = [ - channelscript(channel), + initscript(vm(m), channel), (is_channels_webtransport() ? Genie.Assets.channels_script_tag(channel) : Genie.Assets.webthreads_script_tag(channel)), Genie.Renderer.Html.script(src = Genie.Assets.asset_path(assets_config, :js, file="underscore-min")), Genie.Renderer.Html.script(src = Genie.Assets.asset_path(assets_config, :js, file=(Genie.Configuration.isprod() ? "vue.global.prod" : "vue.global"))), diff --git a/src/Tools.jl b/src/Tools.jl index 0f918d70..8ac1227e 100644 --- a/src/Tools.jl +++ b/src/Tools.jl @@ -205,9 +205,49 @@ macro stipple_precompile(setup, workload) end macro stipple_precompile(workload) + # wrap @app calls in @eval to avoid precompilation errors + for (i, ex) in enumerate(workload.args) + if ex isa Expr && ex.head == :macrocall && ex.args[1] == Symbol("@app") + workload.args[i] = :(@eval $(ex))#Expr(:macrocall, Symbol("@eval"), ex.args) + end + end quote @stipple_precompile begin end begin $workload end end -end \ No newline at end of file +end + +""" + striplines!(ex::Union{Expr, Vector}) + +Remove all line number nodes from an expression or vector of expressions. See also `striplines`. +""" +function striplines!(ex::Expr; recursive::Bool = false) + for i in reverse(eachindex(ex.args)) + if isa(ex.args[i], LineNumberNode) && (ex.head != :macrocall || i > 1) + deleteat!(ex.args, i) + elseif isa(ex.args[i], Expr) && recursive + striplines!(ex.args[i]) + end + end + ex +end + +function striplines!(exprs::Vector; recursive::Bool = false) + for i in reverse(eachindex(exprs)) + if isa(exprs[i], LineNumberNode) + deleteat!(exprs, i) + elseif isa(exprs[i], Expr) && recursive + striplines!(exprs[i]) + end + end + exprs +end + +""" + striplines(ex::Union{Expr, Vector}) + +Return a copy of an expression with all line number nodes removed. See also `striplines!`. +""" +striplines(ex; recursive::Bool = false) = striplines!(copy(ex); recursive) \ No newline at end of file diff --git a/src/stipple/jsmethods.jl b/src/stipple/jsmethods.jl index 646435dd..38ca1656 100644 --- a/src/stipple/jsmethods.jl +++ b/src/stipple/jsmethods.jl @@ -36,10 +36,11 @@ function js_methods(app::T)::String where {T<:ReactiveModel} "" end +# deprecated, now part of the model function js_methods_events()::String """ handle_event: function (event, handler) { - Genie.WebChannels.sendMessageTo(window.CHANNEL, 'events', { + Genie.WebChannels.sendMessageTo(GENIEMODEL.channel_, 'events', { 'event': { 'name': handler, 'event': event @@ -203,9 +204,9 @@ myreviver: function(key, value) { return (key.endsWith('_onebased') ? value - 1 """ function js_add_reviver(revivername::String) """ - Genie.WebChannels.subscriptionHandlers.push(function(event) { + document.addEventListener('DOMContentLoaded', () => Genie.WebChannels.subscriptionHandlers.push(function(event) { Genie.Revivers.addReviver($revivername); - }); + })); """ end @@ -221,9 +222,9 @@ It needs to be added to the dependencies of an app in order to be executed, e.g. """ function js_initscript(initscript::String) """ - Genie.WebChannels.subscriptionHandlers.push(function(event) { + document.addEventListener('DOMContentLoaded', () => Genie.WebChannels.subscriptionHandlers.push(function(event) { $(initscript) - }); + })); """ end diff --git a/src/stipple/reactivity.jl b/src/stipple/reactivity.jl index 89ed9057..cf6ee585 100644 --- a/src/stipple/reactivity.jl +++ b/src/stipple/reactivity.jl @@ -153,10 +153,8 @@ end """ abstract type ReactiveModel end -export @vars, @add_vars, @define_mixin, @clear_cache, clear_cache, @clear_route, clear_route +export @vars, @define_mixin, @clear_cache, clear_cache, @clear_route, clear_route -# deprecated -export @reactive, @reactive!, @old_reactive, @old_reactive! export ChannelName, getchannel const ChannelName = String @@ -184,15 +182,6 @@ const INTERNALFIELDS = [CHANNELFIELDNAME, :modes__] # not DRY but we need a refe ws_disconnected::Stipple.R{Bool} = false end -@mix Stipple.@with_kw mutable struct old_reactive - Stipple.@reactors -end - - -@mix Stipple.@kwredef mutable struct old_reactive! - Stipple.@reactors -end - function split_expr(expr) expr.args[1] isa Symbol ? (expr.args[1], nothing, expr.args[2]) : (expr.args[1].args[1], expr.args[1].args[2], expr.args[2]) end @@ -252,73 +241,115 @@ function find_assignment(expr) assignment end -function parse_expression!(expr::Expr, @nospecialize(mode) = nothing, source = nothing, m::Union{Module, Nothing} = nothing) +function get_varname(expr) expr = find_assignment(expr) - Rtype = isnothing(m) || ! isdefined(m, :R) ? :(Stipple.R) : :R - - (isa(expr, Expr) && contains(string(expr.head), "=")) || - error("Invalid binding expression -- use it with variables assignment ex `@binding a = 2`") + var = expr.args[1] + var isa Symbol ? var : var.args[1] +end - source = (source !== nothing ? String(strip(string(source), collect("#= "))) : "") +function assignment_to_conversion(expr) + expr = copy(expr) + expr.head = :call + pushfirst!(expr.args, :convert) + expr.args[2] = expr.args[2].args[2] + expr +end - var = expr.args[1] - if !isnothing(mode) - mode = mode isa Symbol && ! isdefined(m, mode) ? :(Stipple.$mode) : mode - type = if isa(var, Expr) && var.head == Symbol("::") - # change type T to type R{T} - var.args[2] = :($Rtype{$(var.args[2])}) - else - try - # add type definition `::R{T}` to the var where T is the type of the default value - T = @eval m typeof($(expr.args[2])) - expr.args[1] = :($var::$Rtype{$T}) - Rtype - catch ex - # if the default value is not defined, we can't infer the type - # so we just set the type to R{Any} - :($Rtype{Any}) - end - end - expr.args[2] = :($type($(expr.args[2]), $mode, false, false, $source)) +function let_eval!(expr, let_block, m::Module, is_non_reactive::Bool = true) + Rtype = isnothing(m) || ! isdefined(m, :R) ? :(Stipple.R) : :R + with_type = expr.args[1] isa Expr && expr.args[1].head == :(::) + var = with_type ? expr.args[1].args[1] : expr.args[1] + let_expr = Expr(:let, let_block, Expr(:block, with_type ? assignment_to_conversion(expr) : expr.args[end])) + val = try + @eval m $let_expr + catch ex + with_type || @info "Could not infer type of $var, setting it to `Any`, consider adding a type annotation" + :__Any__ end + + T = val === :__Any__ ? Any : typeof(val) + val === :__Any__ || push!(let_block.args, is_non_reactive ? :(var = $val) : :($var = $Rtype{$T}($val))) + return val, T +end - # if no type is defined, set the type of the default value +# deterimine the variables that need to be evaluated to infer the type of the variable +function required_evals!(expr, vars::Set) + expr isa LineNumberNode && return vars + expr = find_assignment(expr) + # @mixin statements are currently not evaluated + expr === nothing && return vars if expr.args[1] isa Symbol + x = expr.args[1] + push!(vars, x) + end + MacroTools.postwalk(expr.args[end]) do ex + MacroTools.@capture(ex, x_[]) && push!(vars, x) + ex + end + return vars +end + +function parse_expression!(expr::Expr, @nospecialize(mode) = nothing, source = nothing, m::Union{Module, Nothing} = nothing, let_block::Union{Expr, Nothing} = nothing, vars::Set = Set()) + expr = find_assignment(expr) + + Rtype = isnothing(m) || ! isdefined(m, :R) ? :(Stipple.R) : :R + + (isa(expr, Expr) && contains(string(expr.head), "=")) || + error("Invalid binding expression -- use it with variables assignment ex `@in a = 2`") + + source = (source !== nothing ? String(strip(string(source), collect("#= "))) : "") + + # args[end] instead of args[2] because of potential LineNumberNode + var = expr.args[1] + varname = var isa Expr ? var.args[1] : var + + is_non_reactive = mode === nothing + mode === nothing && (mode = PRIVATE) + context = isnothing(m) ? @__MODULE__() : m + + # evaluate the expression in the context of the module and append the corresponding assignment to the let_block + # bt only if var is in the set of required 'vars' + val = 0 + T = DataType + let_block !== nothing && varname ∈ vars && ((val, T) = let_eval!(expr, let_block, m, is_non_reactive)) + + mode = mode isa Symbol && ! isdefined(context, mode) ? :(Stipple.$mode) : mode + type = if isa(var, Expr) && var.head == Symbol("::") && ! is_non_reactive + # change type T to type R{T} + var.args[end] = :($Rtype{$(var.args[end])}) + else # no type is defined, so determine it from the type of the default value try - T = @eval m typeof($(expr.args[2])) - expr.args[1] = :($(expr.args[1])::$T) + # add type definition `::R{T}` to the var where T is the type of the default value + T = let_block === nothing ? typeof(@eval(context, $(expr.args[end]))) : T + expr.args[1] = is_non_reactive ? :($var::$T) : :($var::$Rtype{$T}) + is_non_reactive ? T : Rtype catch ex # if the default value is not defined, we can't infer the type - # so we just set the type to Any - expr.args[1] = :($(expr.args[1])::Any) + # so we just set the type to R{Any} + @info "Could not infer type of $var, setting it to R{Any}" + expr.args[1] = is_non_reactive : :($var::Any) : :($var::$Rtype{Any}) + is_non_reactive ? :($var::Any) : :($Rtype{Any}) end end - expr.args[1].args[1], expr + + is_non_reactive || (expr.args[end] = :($type($(expr.args[end]), $mode, false, false, $source))) + varname, expr end -macro var_storage(expr, new_inputmode = :auto) +parse_expression(expr::Expr, mode = nothing, source = nothing, m = nothing, let_block::Expr = nothing, vars::Set = Set()) = parse_expression!(copy(expr), mode, source, m, let_block, vars) + +macro var_storage(expr) m = __module__ if expr.head != :block expr = quote $expr end end - if new_inputmode == :auto - new_inputmode = true - for e in expr.args - e isa LineNumberNode && continue - e.args[1] isa Symbol && continue - - type = e.args[1].args[2] - if startswith(string(type), r"(Stipple\.)?R(eactive)?($|{)") - new_inputmode = false - break - end - end - end - storage = init_storage() source = nothing + required_vars = Set() + let_block = Expr(:block, :(_ = 0)) + required_evals!.(expr.args, Ref(required_vars)) for e in expr.args if e isa LineNumberNode source = e @@ -327,34 +358,17 @@ macro var_storage(expr, new_inputmode = :auto) mode = :PUBLIC reactive = true if e.head == :(=) - var, ex = if new_inputmode - #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] ∈ [: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) - else - var = e.args[1] - if var isa Symbol - reactive = false - else - type = var.args[2] - reactive = startswith(string(type), r"(Stipple\.)?R(eactive)?($|{)") - var = var.args[1] - end - if occursin(Stipple.SETTINGS.private_pattern, string(var)) - mode = :PRIVATE - elseif occursin(Stipple.SETTINGS.readonly_pattern, string(var)) - mode = :READONLY - end - var, e + #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] ∈ [: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 ∈ keys(Stipple.init_storage()) && continue if reactive == false @@ -412,19 +426,17 @@ macro type(modelname, storage) modelconst = Symbol(modelname, '!') modelconst_qn = QuoteNode(modelconst) + output = quote end + output.args = @eval __module__ collect(values($storage)) + output_qn = QuoteNode(output) + quote abstract type $modelname <: Stipple.ReactiveModel end - local output = quote end - output.args = collect(values($storage)) - # Revise seems to call the macro line by line internally for code tracking purposes. - # Interstingly, Revise will not populate output.args in that case and will generate an empty model. - # We use this to our advantage and prevent additional model generation when length(output.args) <= 1. - local is_called_by_revise = length(output.args) <= 1 - eval(quote - $is_called_by_revise || Stipple.@kwredef mutable struct $$modelconst_qn <: $$modelname - $output - end - end) + + Stipple.@kwredef mutable struct $modelconst <: $modelname + $output + end + $modelname(; kwargs...) = $modelconst(; kwargs...) Stipple.get_concrete_type(::Type{$modelname}) = $modelconst @@ -446,51 +458,13 @@ end e::String = "private", NON_REACTIVE, PRIVATE end ``` -This macro replaces the old `@reactive!` and doesn't need the Reactive in the declaration. -Instead the non_reactives are marked by a flag. The old declaration syntax is still supported -to make adaptation of old code easier. -``` -@vars HHModel begin - a::R{Int} = 1 - b::R{Float64} = 2 - c::String = "Hello" - d_::String = "readonly" - e__::String = "private" -end -``` -by - -```julia -@reactive! mutual struct HHModel <: ReactiveModel - a::R{Int} = 1 - b::R{Float64} = 2 - c::String = "Hello" - d_::String = "readonly" - e__::String = "private" -end -``` - -Old syntax is still supported by @vars and can be forced by the `new_inputmode` argument. - """ -macro vars(modelname, expr, new_inputmode = :auto) +macro vars(modelname, expr) quote - Stipple.@type($modelname, values(Stipple.@var_storage($expr, $new_inputmode))) + Stipple.@type($modelname, values(Stipple.@var_storage($expr))) end |> esc end -macro add_vars(modelname, expr, new_inputmode = :auto) - storage = @eval(__module__, Stipple.@var_storage($expr, $new_inputmode)) - new_storage = if isdefined(__module__, modelname) - old_storage = @eval(__module__, Stipple.model_to_storage($modelname)) - ReactiveTools.merge_storage(old_storage, storage; context = __module__) - else - storage - end - - esc(:(Stipple.@type $modelname $new_storage)) -end - macro define_mixin(mixin_name, expr) storage = @eval(__module__, Stipple.@var_storage($expr)) delete!.(Ref(storage), [:channel__, Stipple.AUTOFIELDS...]) @@ -502,44 +476,6 @@ macro define_mixin(mixin_name, expr) end |> esc end -macro reactive!(expr) - warning = """@reactive! is deprecated, please replace use `@vars` instead. - - In case of errors, please replace `@reactive!` by `@old_reactive!` and open an issue at - https://github.com/GenieFramework/Stipple.jl. - - If you use `@old_reactive!`, make sure to call `accessmode_from_pattern!()`, because the internals for - accessmode have changed, e.g. - ``` - model = init(MyDashboard) |> accessmode_from_pattern! |> handlers |> ui |> html - ``` - """ - @warn warning - output = @eval(__module__, values(Stipple.@var_storage($(expr.args[3]), false))) - expr.args[3] = quote $(output...) end - - esc(:(Stipple.@kwredef $expr)) -end - -macro reactive(expr) - warning = """@reactive is deprecated, please replace use `@vars` instead. - - In case of errors, please replace `@reactive` by `@old_reactive!` and open an issue at - https://github.com/GenieFramework/Stipple.jl. - If you use `@old_reactive!`, make sure to call `accessmode_from_pattern!()`, because the internals for - accessmode have changed, e.g. - ``` - model = init(MyDashboard) |> accessmode_from_pattern! |> handlers |> ui |> html - ``` - - """ - @warn warning - output = @eval(__module__, values(Stipple.@var_storage($(expr.args[3]), false))) - expr.args[3] = quote $(output...) end - - esc(:(Base.@kwdef $expr)) -end - #===# mutable struct Settings diff --git a/src/stipple/rendering.jl b/src/stipple/rendering.jl index 156d078c..fc9c7913 100644 --- a/src/stipple/rendering.jl +++ b/src/stipple/rendering.jl @@ -135,7 +135,7 @@ function Stipple.render(app::M)::Dict{Symbol,Any} where {M<:ReactiveModel} for field in fieldnames(typeof(app)) f = getfield(app, field) - occursin(SETTINGS.private_pattern, String(field)) && continue + field != CHANNELFIELDNAME && occursin(SETTINGS.private_pattern, String(field)) && continue f isa Reactive && f.r_mode == PRIVATE && continue result[field] = Stipple.jsrender(f, field) diff --git a/test/runtests.jl b/test/runtests.jl index dffaf611..22e17548 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -10,7 +10,7 @@ function string_get(x; kwargs...) end function get_channel(s::String) - match(r"window.CHANNEL = '([^']+)'", s).captures[1] + match(r"\(\) => window.create[^']+'([^']+)'", s).captures[1] end function get_debounce(port, modelname)