Skip to content

Commit

Permalink
Merge branch 'hh-multimodel' into hh-appmacros
Browse files Browse the repository at this point in the history
  • Loading branch information
hhaensel committed Nov 30, 2024
2 parents c7ebf41 + df8ec00 commit 2afba1c
Show file tree
Hide file tree
Showing 8 changed files with 154 additions and 101 deletions.
34 changes: 23 additions & 11 deletions assets/js/keepalive.js
Original file line number Diff line number Diff line change
@@ -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)
}
4 changes: 2 additions & 2 deletions assets/js/watchers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
136 changes: 67 additions & 69 deletions src/Elements.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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
"""
)

Expand Down
47 changes: 41 additions & 6 deletions src/Layout.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -41,19 +60,22 @@ julia> layout([
"<link href=\"https://fonts.googleapis.com/css?family=Material+Icons\" rel=\"stylesheet\" /><link href=\"https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,400;0,700;0,900;1,400&display=swap\" rel=\"stylesheet\" /><link href=\"/css/stipple/stipplecore.css\" rel=\"stylesheet\" /><link href=\"/css/stipple/quasar.min.css\" rel=\"stylesheet\" /><span v-text='greeting'>Hello</span><script src=\"/js/channels.js?v=1.17.1\"></script><script src=\"/js/underscore-min.js\"></script><script src=\"/js/vue.global.prod.js\"></script><script src=\"/js/quasar.umd.prod.js\"></script>\n<script src=\"/js/apexcharts.min.js\"></script><script src=\"/js/vue-apexcharts.min.js\"></script><script src=\"/js/stipplecore.js\" defer></script><script src=\"/js/vue_filters.js\" defer></script>"
```
"""
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(
Expand Down Expand Up @@ -84,21 +106,34 @@ julia> page(:elemid, [
"<!DOCTYPE html>\n<html><head><title></title><meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui\" /></head><body class style><link href=\"https://fonts.googleapis.com/css?family=Material+Icons\" rel=\"stylesheet\" /><link href=\"https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,400;0,700;0,900;1,400&display=swap\" rel=\"stylesheet\" /><link href=\"/css/stipple/stipplecore.css\" rel=\"stylesheet\" /><link href=\"/css/stipple/quasar.min.css\" rel=\"stylesheet\" /><div id=elemid><span v-text='greeting'>Hello</span></div><script src=\"/js/channels.js?v=1.17.1\"></script><script src=\"/js/underscore-min.js\"></script><script src=\"/js/vue.global.prod.js\"></script><script src=\"/js/quasar.umd.prod.js\"></script>\n<script src=\"/js/apexcharts.min.js\"></script><script src=\"/js/vue-apexcharts.min.js\"></script><script src=\"/js/stipplecore.js\" defer></script><script src=\"/js/vue_filters.js\" defer></script></body></html>"
```
"""
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
Expand Down
19 changes: 13 additions & 6 deletions src/Stipple.jl
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,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...)
Expand Down Expand Up @@ -342,10 +342,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
Expand Down Expand Up @@ -845,11 +845,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)
Expand All @@ -859,7 +866,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"))),
Expand Down
Loading

0 comments on commit 2afba1c

Please sign in to comment.