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)