ToolipsSession provides Toolips with fullstack web-development capabilities (live calls to the server back and forth from the client on a single page,) through the Session extension. This is considered to be the quintessential Toolips extension, as it was created alongside Toolips and helped to shape the framework itself. ToolipsSession provides a simple call-back style API based on the ClientModifier API from ToolipsServables. This is mainly facilitated through new dispatches to on, bind, and the ComponentModifier.
module FullStackServer
using Toolips
using Toolips.Components
using ToolipsSession
SESSION = ToolipsSession.Session()
home = route("/") do c::AbstractConnection
name_input = Components.textdiv("namebox", text = "enter your name for a greeting", align = "left")
style!(name_input, "display" => "inline-block", "padding" => 7px, "background-color" => "white", "color" => "#1e3ddd",
"border-style" => "solid", "border-width" => 2px, "border-color" => "#1e1e1e", "width" => 80percent)
# ToolipsSession `on` binding (includes `Connection`):
on(c, name_input, "focus") do cm::ComponentModifier
# indexing `ComponentModifier`
if contains(cm[name_input]["text"], " ")
set_text!(cm, name_input, "")
end
end
# defining multi-use `ComponentModifier` Function:
function generate_namepage(cm::ComponentModifier)
# Component already on page? An easy check for that:
if "namepage" in cm
return
end
closebutton = button("close", text = "close")
# non-Session event (`ClientModifier`, no `Connection` provided.) Evaluated as soon as its called,
# is only a client-side callback
on(closebutton, "click") do cl::ClientModifier
remove!(cl, "namepage")
end
# we can also reference components by name
# properties will always be a `String`, and can be parsed into values in callbacks.
name::String = cm["namebox"]["text"]
closebar = div("-", align = "right", children = [closebutton])
namegreeter = h2(text = name)
name_sub = p(text = "what a pretty name you have!")
namepage = div("namepage", children = [closebar, namegreeter, name_sub])
style!(namepage, "position" => "absolute", "top" => 0percent, "width" => 70percent, "height" => 0percent, "opacity" => 0percent,
"transition" => 800ms, "padding" => 10percent)
append!(cm, "mainbody", namepage)
on(cm, 600) do cl::ClientModifier
style!(cl, "namepage", "height" => 100percent, "opacity" => 100percent)
end
end
# ToolipsSession `bind` binding:
ToolipsSession.bind(generate_namepage, c, name_input, "Enter", prevent_default = true)
confirm_button = button("conf", text = "confirm")
# duplicated binding :)
on(generate_namepage, c, confirm_button, "click")
style!(confirm_button, "background-color" => "#333333", "padding" => 7px, "border" => "2px solid #1e1e1e",
"color" => "white")
content_box = div("contentbox", align = "center", children = [name_input, confirm_button])
style!(content_box, "padding" => 30percent, "margin-top" => 5percent)
main_body = body("mainbody", children = [content_box])
write!(c, main_body)
end
export SESSION, home
end
Note that Session requires a Toolips (HTTP) server to run.
In order to use bind callback events to pages, we are first going to need to load the Session extension into our Toolips server. The main thing the Session extension does is write server information on the client for communcation on pages it is active. This is important, because it means that Session needs to delineate what what pages to write this to — we don't want Session writing this on a page where we are meant to serve files. For this reason, we must provide active_routes when creating Session. We are able to actively change these routes, or invert Session behavior so that the active_routes become inactive_routes, but generally we will need to put all of our target paths into active_routes. We start by creating and exporting Session in our Module, optionally with a list of active_routes or key-word arguments. By default, the active_routes will be ["/"]. These may also be inverted using a key-word argument.
module SessionMin
using Toolips
using Toolips.Components
using ToolipsSession
SESSION = ToolipsSession.Session(["/", "/forum"])
export SESSION
end
The timeout binding is an Integer representing how many minutes we wait before deleting a client's active events. By default, this is 10 minutes.
From here, we add Events, sub-types of AbstractEvent that are normally a regular Event, these events are added using the on and bind functions, amongst other options. Each of the following functions can be used to register an event in different ways (click to view function documentation).
ToolipsSession comes with only a couple event types, the most common is the regular Event. Here is a comprehensive list:
Events are routed by Session to the callbacks on your web-page, the callbacks are registered an ID along with your response. The events are then called using call!. This isn't always done with the register! function, but most often this is the case. ToolipsSession also provides a binding to kill! (kill!(c::AbstractConnection)) which will remove a user's events from the current session. There is also clear!, which will only remove the client's events — not deleting them from the IP table. Beyond this, most of ToolipsSession usage will remain in the high-level on and bind functions.
The main feature that ToolipsSession provides to Toolips is full-stack 'ComponentModifier' callbacks. These are callbacks which call the server, as opposed to operating exclusively on a client. For example, the popup documentation you are reading like this are added to the page using a ComponentModifier callback, the callback calls the server and generates a documentation popup for the name associated with that popup, in this case "sample".
Callbacks are registered Events, which we create by calling on or bind on a Component or a Connection.
bind docs
bind(chnl::Channel, task::Task)Associate the lifetime of
chnlwith a task.Channelchnlis automatically closed when the task terminates. Any uncaught exception in the task is propagated to all waiters onchnl.The
chnlobject can be explicitly closed independent of task termination. Terminating tasks have no effect on already closedChannelobjects.When a channel is bound to multiple tasks, the first task to terminate will close the channel. When multiple channels are bound to the same task, termination of the task will close all of the bound channels.
Examples
julia> c = Channel(0); julia> task = @async foreach(i->put!(c, i), 1:4); julia> bind(c,task); julia> for i in c @show i end; i = 1 i = 2 i = 3 i = 4 julia> isopen(c) falsejulia> c = Channel(0); julia> task = @async (put!(c, 1); error("foo")); julia> bind(c, task); julia> take!(c) 1 julia> put!(c, 1); ERROR: TaskFailedException Stacktrace: [...] nested task error: foo [...]bind(socket::Union{TCPServer, UDPSocket, TCPSocket}, host::IPAddr, port::Integer; ipv6only=false, reuseaddr=false, kws...)Bind
socketto the givenhost:port. Note that0.0.0.0will listen on all devices.
The
ipv6onlyparameter disables dual stack mode. Ifipv6only=true, only an IPv6 stack is created.If
reuseaddr=true, multiple threads or processes can bind to the same address without error if they all setreuseaddr=true, but only the last to bind will receive any traffic.on docs
on(f::Function, ...) -> ::Nothing/::Component{:script}on is used to register events to components or directly to pages using Javascript's EventListeners. on will generally be passed a Component and an event.
on(f::Function, component::Component{<:Any}, event::String) -> ::Nothing
on(f::Function, event::String) -> ::Component{:script}# call in a certain amount of time, rather than on an event.
on(f::Function, comp::AbstractComponentModifier, perform_in::Integer; recurring::Bool = false)
on(f::Function, perform_in::Integer; recurring::Bool = false)
See also: ClientModifier,
move!,remove!,append!,set_children!, bind,ToolipsServablesmodule MyServer
using Toolips
using Toolips.Components
home = route("/") do c::Connection
mybutton = div("mainbut", text = "click this button")
style!(mybutton, "border-radius" => 5px)
on(mybutton, "click") do cl::ClientModifier
alert!(cl, "hello world!")
end
write!(c, mybutton)
end
export home
endon(f::Function, session::Session, name::String) -> ::NothingThis binding is used to bind events and save them for later, referencing them by their provided
name.on(sess, "sample") do cm::ComponentModifier
style!(cm, "samp", "color" => "blue")
end
main = route("/") do c::Toolips.AbstractConnection
mainbody = body("mainbod")
on("sample", c, mainbody, "click")
write!(c, mainbody)
endon(name::String, ...; prevent_default::Bool = false) -> ::NothingThese on bindings are used to bind existing events, registered using
on(::Function, ::Session, ::String)to components andConnections. This makes it possible to create reusable global bindings for each client, for callbacks that have no variation and don't require function variables. Anytime theConnectionis provided, this will be a server-side callback.on(name::String, c::AbstractConnection, event::String;
prevent_default::Bool = false)
on(name::String, comp::Component{<:Any}, event::String;
prevent_default::Bool = false)
on(f::Function, cm::AbstractComponentModifier, comp::Component{<:Any}, event::String;
prevent_default::Bool = false)module SampleServer
using Toolips
using Toolips.Components
using Toolips
session = Session()
on(sess, "sample") do cm::ComponentModifier
style!(cm, "samp", "color" => "blue")
end
main = route("/") do c::Toolips.AbstractConnection
mainbody = body("mainbod")
on("sample", c, mainbody, "click")
write!(c, mainbody)
end
export main, session
endon(f::Function, c::AbstractConnection, args ...; prevent_default::Bool = false)ToolipsSession extends on, providing each on dispatch with a
Connectionequivalent that makes a callback to the server. This allows us to access data, or do more calculations than would otherwise be possible with a ClientModifier.on(f::Function, c::AbstractConnection, event::AbstractString; prevent_default::Bool = false)
on(f::Function, c::AbstractConnection, s::AbstractComponent, event::AbstractString,
prevent_default::Bool = false)
on(f::Function, c::AbstractConnection, cm::AbstractComponentModifier, event::AbstractString)
on(f::Function, c::AbstractConnection, cm::AbstractComponentModifier, comp::Component{<:Any},
event::AbstractString)
See also: script!,
ToolipsSession.bind, KeyMap, open_rpc!, join_rpc!, Session, ToolipsSession, ComponentModifiermodule SampleServer
using Toolips
using Toolips.Components
session = Session()
main = route("/") do c::AbstractConnection
txtbox = textdiv("entertxt")
style!(txtbox, "width" => 10percent, "border-width" => 3px, "border-color" => "gray", "border-style" => "solid")
end
export main, session
end
It is also possible to bind global events without a Connection by calling on using the Session extension directly. There is a specific section in the on documentation dedicated to these dispatches.
# on(f::Function, session::Session, name::String)
module SampleServer
using Toolips
using ToolipsSession
SESSION = ToolipsSession.Session()
# create global event
on(SESSION, "clickbutton") do cm::ComponentModifier
style!(cm, "mainbody", "background-color" => "blue")
end
home = route("/") do c::AbstractConnection
# bind our global event
change_button = button("changer", text = "change color")
on("clickbutton", change_button, "click")
mainbody = body("mainbody", children = [change_button], align = "center")
style!(mainbody, "transition" => 5seconds, "padding-top" => 5percent)
write!(c, mainbody)
end
Another really cool feature that comes packaged in ToolipsSession is RPC functionality. For managing RPC sessions, we use the following functions:
open_rpc! for creating an RPC session from the current client
reconnect_rpc! for reconnecting clients( also done automatically by open_rpc! and join_rpc!)
To call events across multiple clients in the same RPC session, we then use rpc! and call!. rpc! is used to call events on all clients, call! is used to call events on all other clients, OR by providing an IP (String)we can call! on a specific client. Both of these will clear the ComponentModifier changes when they are finished, meaning that the events will be removed. So, for example, we could perform a call! and an rpc!, and then have another specific event for the client all in the same callback.
Here is an example, implementing 'Connect 4' with ToolipsSession RPC. This is certainly a little intense, but contains examples most RPC calls we would need to build just about any project we would want:
module Connect4
using Toolips
using Toolips.Components
using ToolipsSession
# extensions
logger = Toolips.Logger()
SESSION = ToolipsSession.Session(["/", "/game"])
# we will eventually fill `GAMES` with the currently active games.
# we will add them using `push!`, pushing a `Pair{String, Pair{String, Int64}}` ("username" => "ip" => n_players)
mutable struct GameData
ip::String
players::UInt8
fills::Matrix{Int8}
peer::String
turn::Bool
end
GAMES = Dict{String, GameData}()
function games_list(c::Toolips.AbstractConnection)
# styles:
header_label = style("div.headerlabel", "padding" => 10px, "font-size" => 16pt, "color" => "white", "font-weight" => "bold",
"width" => 50percent)
obs_label = style("div.obslabel", "padding" => 10px, "font-size" => 16pt, "color" => "#333333", "width" => 50percent,
"pointer-events" => "none")
active_server = style("div.active-server", "display" => "inline-flex", "width" => 100percent, "transition" => 600ms,
"cursor" => "pointer")
active_server:"hover":["transform" => "scale(1.06)", "border-left" => "3px solid green"]
write!(c, header_label, obs_label, active_server)
# i set the class of our new sections to our style names, provide them as children to `header_box`
section1, section2 = div("-", text = "name", class = "headerlabel"), div("-", text = "players", class = "headerlabel")
header_box = div("list-header", children = [section1, section2])
# style header box
style!(header_box, "background-color" => "darkblue", "display" => "inline-flex", "width" => 100percent)
# build list_items — the selectable components that represent the current games.
list_items = [begin
name = game[1]
gameinfo = game[2]
game_ip = gameinfo.ip
n_players = gameinfo.players
section1, section2 = div("-", text = name, class = "obslabel"), div("-", text = "$(n_players)/2", class = "obslabel")
user_box = div(name, children = [section1, section2], class = "active-server")
on(c, user_box, "dblclick") do cm::ComponentModifier
GAMES[name].peer = get_ip(c)
redirect!(cm, "/game")
end
if n_players > 1
style!(user_box, "border-left" => "5px solid red")
else
style!(user_box, "background-color" => "white")
end
user_box
end for game in GAMES]
# new game button:
new_game = div("newgame", text = "create new game", align = "center")
style!(new_game, "width" => 98percent, "padding" => 1percent, "font-weight" => "bold", "background-color" => "darkgreen",
"color" => "white", "font-size" => 15pt, "cursor" => "pointer")
on(c, new_game, "click") do cm::ComponentModifier
if "newgdialog" in cm
return
end
inputbox = Components.textdiv("name-input")
style!(inputbox, "background-color" => "white", "color" => "black", "border" => "2px solid #333333")
confirm_butt, cancel_butt = button("confirm", text = "confirm"), button("cancel", text = "cancel")
on(c, confirm_butt, "click") do cm::ComponentModifier
game_name = cm["name-input"]["text"]
fills = hcat([[0 for y in 1:6] for x in 1:7] ...)
new_game_data = GameData(get_ip(c), UInt8(1), fills, "", true)
push!(GAMES, game_name => new_game_data)
redirect!(cm, "/game")
end
on(c, cancel_butt, "click") do cm::ComponentModifier
end
new_game_dialog = div("newgdialog", children = [inputbox, confirm_butt, cancel_butt])
style!(new_game_dialog, "position" => "absolute", "left" => 35percent, "top" => 45percent,
"width" => 30percent, "padding" => 10px, "z-index" => 5,
"display" => "inline-block", "opacity" => 0percent, "transform" => "translateY(10%)", "background-color" => "darkred",
"transition" => 650ms)
name_header = h3(text = "name your game")
append!(cm, "mainbody", new_game_dialog)
on(cm, 300) do cm2::ClientModifier
style!(cm2, "newgdialog", "opacity" => 100percent, "transform" => translateY(0percent))
focus!(cm2, "name-input")
end
end
# assemble our header_box (and list items) (and now new game button) into a dialog:
dialog_window = div("main-dialog", children = [header_box, list_items ..., new_game])
style!(dialog_window, "position" => "absolute",
"width" => 30percent, "top" => 16percent, "left" => 35percent,
"border-radius" => 3px, "border" => "2px solid #333333")
# compose into body and return:
mainbody = body("mainbody", children = [dialog_window])
end
# our main route, simply writes the returned dialog from `games_list`.
main = route("/") do c::Toolips.AbstractConnection
write!(c, games_list(c))
end
get_game(ip::String) = begin
f = findfirst(gameinfo -> gameinfo.ip == ip, GAMES)
if ~(isnothing(f))
return(GAMES[f], true)
end
f = findfirst(gameinfo -> gameinfo.peer == ip, GAMES)
if ~(isnothing(f))
return(GAMES[f], false)
end
return(nothing, false)
end
clear_game!(ip::String) = begin
f = findfirst(gameinfo -> gameinfo.ip == ip, GAMES)
if ~(isnothing(f))
delete!(game, f)
end
f = findfirst(gameinfo -> gameinfo.peer == ip, GAMES)
if ~(isnothing(f))
delete!(game, f)
end
end
game = route("/game") do c::Toolips.AbstractConnection
ip::String = get_ip(c)
gameinfo, is_host::Bool = get_game(ip)
if isnothing(gameinfo)
scr = on(100) do cl::ClientModifier
redirect!(cl, "/")
end
write!(c, scr)
return
end
if is_host
open_rpc!(c)
else
join_rpc!(c, gameinfo.ip)
end
build_game(c, gameinfo, is_host)
end
function build_game(c, gameinfo, is_host)
connect_container = div("connect-main")
over_container = svg("connect-over")
turn_indicator = div("turn-indicator")
style!(turn_indicator, "background-color" => "white", "border-radius" => 6px, "color" => "darkblue",
"font-weight" => "bold", "font-size" => 17pt, "padding" => 5px, "border" => "2px solid #dddddd")
num = 1
if ~(is_host)
num = 2
end
if gameinfo.turn && ~(is_host)
turn_indicator[:text] = "host's turn"
elseif gameinfo.turn && is_host
turn_indicator[:text] = "your turn"
elseif ~(gameinfo.turn) && ~(is_host)
turn_indicator[:text] = "your turn"
elseif ~(gameinfo.turn) && is_host
turn_indicator[:text] = "challenger's turn"
end
common = ("position" => "absolute", "cdcscd" => "null")
style!(connect_container, "background-color" => "darkblue", "height" => 75percent, "padding" => 5percent,
"width" => 90percent, "left" => 0percent, "position" => "absolute", "top" => 5percent)
style!(over_container, "background-color" => "transparent", "height" => 78percent, "top" => 0.2percent, "width" => 75percent,
"left" => 5percent, common ...)
n = size(gameinfo.fills)
xpercentage = 70 / n[2]
ypercentage = 80 / n[1]
color = "darkred"
if is_host
color = "#9b870c"
end
placement_previews = [begin
cx_value = (xpercentage * e * 10) * percent
circ = Component{:circle}("active_circ$e", cx = cx_value, cy = 6percent, r = 40)
style!(circ, "fill" => color, "opacity" => 10percent, "cursor" => "pointer", "transition" => 750ms)
on(c, circ, "dblclick") do cm
full_col = findlast(n -> n == 0, gameinfo.fills[:, e])
if isnothing(full_col)
alert!(cm, "that column is full")
return
end
gameinfo.fills[full_col, e] = num
four_match = find_four_in_a_row(gameinfo.fills, num)
style!(cm, "circ-$e-$full_col", "fill" => color)
if length(four_match) > 0
name = "host"
if num == 2
name = "client"
end
[begin
style!(cm, "circ-$(match[2])-$(match[1])", "stroke" => "darkgreen", "stroke-width" => 4px)
end for match in four_match[1]]
popupbox = div("popupbox", text = "$name wins!", align = "center")
style!(popupbox, "background-color" => color, "color" => "#dd3ddd", "width" => 10percent,
"top" => 40percent, "height" => 500px, "padding" => 3percent, "left" => 90percent, "font-size" => 22pt,
"z-index" => 20)
append!(cm, "main-bod", popupbox)
on(cm, 3000) do cl::ClientModifier
redirect!(cl, "/")
end
rpc!(c, cm)
clear_game!(get_ip(c))
return
end
rpc!(c, cm)
gameinfo.turn = ~gameinfo.turn
call!(c, cm) do cm2::ComponentModifier
set_text!(cm2, "turn-indicator", "your turn")
style!(cm2, "connect-over", "pointer-events" => "auto")
end
style!(cm, "connect-over", "pointer-events" => "none")
set_text!(cm, "turn-indicator", "opponent's turn")
end
on(c, circ, "mouseenter") do cm::ComponentModifier
style!(cm, circ, "opacity" => 100percent)
rpc!(c, cm)
end
on(c, circ, "mouseleave") do cm::ComponentModifier
style!(cm, circ, "opacity" => 10percent)
rpc!(c, cm)
end
circ
end for e in 1:n[2]]
set_children!(over_container, placement_previews)
circles = vcat([begin
[begin
circ = Component{:circle}("circ-$column_n-$row_n", r = 40, cx = (xpercentage * column_n * 10) * percent,
cy = (ypercentage * row_n * 5) * percent)
if fillvalue == 0
style!(circ, "fill" => "white")
elseif fillvalue == 1
style!(circ, "fill" => "#9b870c")
elseif fillvalue == 2
style!(circ, "fill" => "darkred")
end
circ
end for (row_n, fillvalue) in enumerate(fillcolumn)]
end for (column_n, fillcolumn) in enumerate(eachcol(gameinfo.fills))] ...)
main_vector = svg("main-svg", width = 100percent, height = 90percent, children = circles)
style!(main_vector, "background-color" => "lightblue", "border-radius" => 3px, "width" => 75percent,
"left" => 5percent, "pointer-events" => "none", "top" => 0percent, common ...)
push!(connect_container, main_vector, over_container)
mainbody = body("main-bod", children = [turn_indicator, connect_container])
style!(mainbody, "background-color" => "darkblue")
write!(c, mainbody)
end
function find_four_in_a_row(matrix::Array{<:Integer, 2}, target::Integer)
rows, cols = size(matrix)
# Define the directions: (row increment, column increment)
directions = [
(0, 1), # Horizontal
(1, 0), # Vertical
(1, 1), # Diagonal (top-left to bottom-right)
(1, -1) # Diagonal (top-right to bottom-left)
]
# To store the indices of the found sequences
sequences = Vector{Vector{Tuple{Int, Int}}}()
for r in 1:rows
for c in 1:cols
for (dr, dc) in directions
# Check if 4 in a row is possible in this direction
if r * 3 * dr <= rows && r * 3 * dr >= 1 && c * 3 * dc <= cols && c * 3 * dc >= 1
if all(matrix[r * i * dr, c * i * dc] == target for i in 0:3)
# Collect the indices of the matching sequence
indices = [(r * i * dr, c * i * dc) for i in 0:3]
push!(sequences, indices)
end
end
end
end
end
return sequences
end
export main, default_404, logger, SESSION, game
end # - module Connect4 <3
Another important feature to take note of is the InputMap. An InputMap is a constructed type that is built to be used with bind. ToolipsSession provides the following two input maps (click to view documentation):
These will start with a constructor call, making a KeyMap or a SwipeMap. Then we use bind just as we would on a Connection on the InputMap and then finish by binding the InputMap to the Connection
using Toolips
using ToolipsSession
main = route("/") do c::AbstractConnection
km = ToolipsSession.KeyMap()
end
To add to the extensive list of Toolips capabilities, ToolipsSession offers some additional callback bindings. Here is a list of the additional bindings provided by ToolipsSession:
set_selection!
pauseanim!
playanim!
free_redirects!
confirm_redirects!
scroll_to!
scroll_by!
This is in addition to the bindings provided by Toolips.Components.
ToolipsSession (as of right now,) also provides a random Component, the button_select. This Component allows us to make an option selection menu very easily.
This will be moved to toolips servables with the release of ToolipsSession 0.5.