hometoolipsToolipsSession
search
search
ToolipsSession

fullstack

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.

loading session

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.

ComponentModifier callbacks

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 chnl with a task. Channel chnl is automatically closed when the task terminates. Any uncaught exception in the task is propagated to all waiters on chnl.

The chnl object can be explicitly closed independent of task termination. Terminating tasks have no effect on already closed Channel objects.

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)
false
julia> 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 socket to the given host:port. Note that 0.0.0.0 will listen on all devices.

  • The ipv6only parameter disables dual stack mode. If ipv6only=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 set reuseaddr=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, ToolipsServables

module 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
end
on(f::Function, session::Session, name::String-> ::Nothing

This 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)
end
on(name::String, ...; prevent_default::Bool = false-> ::Nothing

These on bindings are used to bind existing events, registered using on(::Function, ::Session, ::String) to components and Connections. 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 the Connection is 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
end
on(f::Function, c::AbstractConnection, args ...; prevent_default::Bool = false)

ToolipsSession extends on, providing each on dispatch with a Connection equivalent 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)
module 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

global events

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

RPC

Another really cool feature that comes packaged in ToolipsSession is RPC functionality. For managing RPC sessions, we use the following functions:

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.

connect 4

  • 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:6for 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&#37;)", "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{<:Integer2}, target::Integer)
    rows, cols 
= size(matrix)

    
# Define the directions: (row increment, column increment)
    directions = [
        (
01),   # Horizontal
        (10),   # Vertical
        (11),   # 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

input maps

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

callback bindings

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!

  • next!

This is in addition to the bindings provided by Toolips.Components.

random component

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.

chifidocs is currently offline; you are viewing a cached version of the site. Some functionality might be missing. Click this message to try for a live version of chifidocs again.