Make sure to see the main readme for more in-depth information.
$ docker pull charje/issr-server
$ docker run --rm \
# this is needed for file upload
- v /tmp:/tmp \
# this is needed for running hooks (including file upload)
-v ~/.local/share/issr/:~/.local/share/issr/ \
# optional configuration file
-v config.lisp:~/.config/issr/config.lisp \
# optional environment variable configuration
-e <environment variable configuration> \
issr-server
The important thing is that your :application-destination
configuration slot can get through to the correct network.
You can do this with network sharing, docker compose, or using a special docker hostname.
networks:
shared:
name: shared
my-http-server:
build: .
networks:
- shared
ports:
- 8080:8080
services:
issr:
image: charje/issr-server
environment:
ISSR_APPLICATION: my-http-server:8080
networks:
- shared
ports:
- 3000:3000
see https://github.com/interactive-ssr/tally-demo for a full example.
This one only works on linux. Use the special network host
.
docker run --network host charje/issr-server
Only for Windows and Mac. Docker has special dns to access host network.
Configure the :application-destination
to be something like host.docker.internal:8080
.
Download latest release.
$ ./gnu/bin/issr-server <config> &> logfile &
$ guix install issr-server
$ issr-server <config> &> logfile &
This will probably be a service one day that you can put in your /etc/config.scm
and deploy.
To get started, you don’t need any configuration; just run your development HTTP server on port 8080
and access it at http://localhost:3000.
It is important to run your HTTP server without TLS. If you want encryption (https), Use the SSL configuration options.
For more information about configuration, see configuration document.
Call Re-Render:
<input name="x" value="1" type="number"/>
<input name="y" value="2" type="number"/>
<button action="add" onclick="rr(this)">
I am add button
</button>
Now your server should have an argument who’s name is “add”. All other named elements on the page will be query-string arguments too. Clicking this button will send re-render the page equivalent to this query string: x=1&y=2&add=t
.
The name attribute of any all the named arguments will be unique in the query string. For example the following is equivalent to the previous example (using JSON):
<button onclick="rr({action:'add',value:'t'})">
I am add button
</button>
rr
can also take no arguments.
The difference between attribute name
and attribute action
is that action
will only be included in the query string if is an argument to rr
. Named elements will appear regardless. Only one action
can be sent at a time. Actions and Names share the same namespace so you should not use the string to identify an action and a name.
This is for when you want to display data to your user. Just put the variable where you want it.
<p>{my-variable}</p>
This is for when you want to get data from your user. Just name it!
<input name="my-variable"/>
In your handler you will have an argument called my-variable
. Depending on how soon you want this data, you can add an rr
.
<input
name="my-variable"
<!-- every time the user types -->
oninput="rr()"
<!-- when the user focuses on another element of the page -->
onblur="rr()"
<!-- 800 milliseconds after the user is done typing -->
oninput="drr(this.id)()"
<!-- 300 milliseconds after the user is done typing -->
oninput="drr(this.id, 300)()"/>
This is when you want to get input from your user, but you want to control it is some way. For the most part, you just do the previous 2 strategies together. Sometimes, this does not quite work. To remedy this, include the update
attribute when the processed data is different than the raw data.
<input name="data" value={data} oninput="rr()"
update={if (rawdata != data) "t"
else ""}/>
See Input Control for a much more in-depth example
<input type="radio" name="direction" value="north" selected="true"/><label for="north">North</label><br />
<input type="radio" name="direction" value="east" /><label for="east">East</label><br />
<input type="radio" name="direction" value="south" /><label for="south">South</label><br />
<input type="radio" name="direction" value="west" /><label for="west">West</label><br />
<button action="submit" onclick="rr(this)">
Submit
</button>
Pressing the submit button will produce a query string like so: submit=t&direction=north
. If no radio button is selected, the name for the radio button will not appear in the query string at all.
<input type="file" name="your-file" />
<button action="upload" onclick="rr(this)">
Upload
</button>
On the server the your-file
http parameter will be set to a json with the following schema
{
"file": string, // what the file is named on the server
"name": string, // what the file was named on the client
"content-type": string // the http content type of the file
}
This value can be customized and acted upon using the file-upload-hook provided by your language’s ISSR module.
<a onclick="rr({action:'smap',value:event.offsetX+','+event.offsetY})">
<img src="picture.png" ismap />
</a>
Clicking the picture will produce produce include smap=x,y
in the query string where x,y
is the coordinate of the click and 0,0
is the top left corner of the picture (y is inverted).
<img src="picture.png" usemap="#cmap"
width="200" height="200"/>
<map name="cmap">
<area action="cmap" value="top-left" onclick="rr(this)"
shape="rect" coords="0,0,100,100">
<area action="cmap" value="top-right" onclick="rr(this)"
shape="rect" coords="100,0,200,100">
<area action="cmap" value="bottom-left" onclick="rr(this)"
shape="rect" coords="0,100,100,200">
<area action="cmap" value="bottom-right" onclick="rr(this)"
shape="rect" coords="100,100,200,200">
</map>
Clicking the top left quadrant of the picture will include cmap=top-left
in the query string. See area-tag for more options to define differently shaped areas.
The trick to keeping state is to require reusable components to have the id
attribute. This not only make it unique, but it gives us a string to use as a HTTP parameter or session variable. The HTTP parameter can remember any state required by the component. The reason this works is because it is not required for the web programmer to specify HTTP parameters. The person using a Component doesn’t need to worry about the existence of this variable (only not to use it for something else).
The tab-box and tab tags will never make it to the client.
<tab-box id="tb-one">
<tab title="Lorem">
<p>
Sit amet...
</p>
</tab>
<tab title="Ipsum">
<p>
Nullam...
</p>
</tab>
<tab title="Dolor">
<p>
Pellentesque...
</p>
</tab>
</tab-box>
For this example will use the Common Lisp with Hunchentoot and markup libraries for HTTP and HTML generation respectively.
First, we make the tab tag. The only purpose of this is to not make the user type a colon and ensure that the title
attribute is present because the title
text will be used for the tab buttons.
(deftag tab (children &key (title (error "tab must have a title")))
<:tab title=title >
,@children
</:tab>)
Second, we make the tab-box tag (id
attribute required),
(deftag tab-box (children &key (id (error "tab-box must have an id"))
title class style)
Get list of tab titles, and decide the active one based on the HTTP parameter.
(let* ((tabs (mapcar
;; get the tag titles
(lambda (tab)
(cdr (assoc "title"
(xml-tag-attributes tab)
:test #'string=)))
(remove-if-not
;; remove whitespace and comment elements
(lambda (child)
(typep child 'xml-tag))
children)))
(active (or (parameter id) (first tabs))))
Let id
class
and style
attributes fall through to the encompassing div
and put a bold title
if it was provided.
<div id=id class=(str:join " " (cons "tab-box" class))
style=style >
,(when title
<merge-tag>
<b>,(progn title)</b>
<br/>
</merge-tag>)
Put a nav
tag to hold the tab buttons. The action
attribute will become the HTTP parameter with the value of whatever tab is selected. The name
attribute will “remember” which tab we are on when we are not clicking tabs. the onclick
will send the value
to the server through the action
attribute (which is whatever id is id).
<nav>
,@(mapcar
(lambda (tab)
<button action=id
name=(when (string= tab active)
id)
value=tab
selected=(string= tab active)
onclick="rr(this)">
,(progn tab)
</button>)
tabs)
</nav>
Dump out the children of the tab
tags out wrapped in div
class
“tab-content”, so we can use CSS to chose which ones to hide and show.
,@(mapcar
(lambda (tab child)
<div selected=(string= tab active)
class="tab-content">
,@(xml-tag-children child)
</div>)
tabs
(remove-if-not
;; remove whitespace or comment elements
(lambda (child)
(typep child 'xml-tag))
children))
</div>))
Lastly, add some CSS to hide the tab content that is not selected. Also lots of stuff to make it look pretty. Some dynamic variables to add customization can’t hurt either. The most important thing is the display: none
and display: block
.
.tab-box {
--border-color: black;
--background-color: white;
--tab-color: lightgrey;
background: var(--background-color);
padding: .7rem;
width: fit-content;
margin: .5rem;
border-radius: 5px;
box-shadow: 0 0 3px black;
}
.tab-box > nav {
color: inherit;
padding: 0 .5rem 0 .5rem;
border-bottom: 1px solid var(--border-color);
}
.tab-box > b {font-size: 1.3rem;}
.tab-box > nav > button {
color:inherit;
position: relative;
bottom: -1px;
margin-bottom: 0;
border: 1px solid var(--border-color);
border-radius: 6px 6px 0 0;
background-color: var(--tab-color);
cursor: pointer;
}
.tab-box > nav > button:focus {outline: none;}
.tab-box > nav > button[selected] {
background-color: var(--background-color);
border-bottom: 1px solid var(--background-color);
cursor: default;
}
.tab-box > .tab-content {display: none;}
.tab-box > .tab-content[selected] {
display: block;
animation: fade 1s;
animation-delay: .0001s;
animation-fill-mode: both;
}
@keyframes fade {
0% {opacity: 0}
100% {opacity: 1}
}
The way to do control what users can input into text boxes is to use the oninput
event. The only issue with this is that if you are disabling some characters to be input, the final result will be the same as the original (empty) input. The solution is to use the update
attribute which, if present, will force all attributes to be updated by the server.
We don’t want the user to be able to enter anything but numbers, and we will put the hyphens in for them.
<input-phonenumber name="phone" value=phone />
First we will define a tag and create a local variable which is the user entered value with all the non-numbers removed and passed through our add-hyphens
function.
(deftag input-phonenumber (&key name value)
(let ((filtered (add-hyphens
(ppcre:regex-replace-all "[^0-9]" value ""))))
Next put the input tag with filtered value and the update
if the value has changed. Just pass through the name
attribute
<input name=name value=filtered
update=(string/= value filtered)
oninput="rr()" />))
Lastly we have to define our add-hyphens
function. It also makes sure that the length is no longer than 12 (numbers plus hyphens).
(defun add-hyphens (number)
(let ((length (length number)))
(cond
;; missing first hyphen
((and (<= 4 length)
(char/= #\- (elt number 3)))
(add-hyphens (str:concat (subseq number 0 3) "-"
(subseq number 3))))
;; missing second hyphen
((and (<= 8 length)
(char/= #\- (elt number 7)))
(add-hyphens (str:concat (subseq number 0 7) "-"
(subseq number 7))))
(:else
(str:substring 0 12 number)))))