Readers of the previous article A radical approach to application development may have rightly noted that the article is too theoretical. Therefore, I hasten to restore the balance of good and evil theory and practice.
This article reveals only the tip of the iceberg called picoLisp. Overboard were interesting moments concerning the interior of the database, the organization of a distributed database, debugging, functional I / O, an object model with multiple inheritance, PicoLisp Prolog ...
I still hope that domestic programmers will look at this powerful tool.
')
Carefully, under the cut a lot of text and brackets!
abu@software-lab.deWeb application development in PicoLisp
© Software Lab. Alexander Burger
This document provides an introduction to writing web applications in PicoLisp.
It concentrates on the XHTML / CSS framework (unlike previous Java-AWT, Java-Swing, and plain-HTML frameworks), which is easier to use, more flexible in design, and independent of plug-ins, JavaScript, cookies, or CSS.
A simple HTTP / HTML GUI has several advantages: it works in any browser and can be completely controlled by a script ("@ lib / scrape.l").
More precisely, CSS
can be used to improve the layout. And browsers
with javascript will respond faster and more smoothly. But this framework works fine in browsers that do not know anything about CSS or JavaScript. All examples were also tested in a w3m text browser.
PicoLisp system basic information:
PicoLisp Reference and
PicoLisp Tutorial . Knowledge of HTML and some CSS and HTTP are assumed.
The examples assume that PicoLisp was launched from a global installation (see
Installation ).
Static pages
PicoLisp can be used to create static HTML pages. This in itself does not make much sense, since you can just as well write HTML-code, but it forms the basis for interactive applications and allows us to get acquainted with the application server and other basic concepts.
Hello world
To start with a minimal application, create a file “project.l” in the PicoLisp installation directory and enter the following two lines.
######################################################################## (html 0 "Hello" "@lib.css" NIL "Hello World!" ) ########################################################################
(We will use and modify this file in all of the following examples. Whenever you meet such a program fragment between the lines ('#####'), just copy and paste it into the project.l file and click on the button “Refresh” your browser to view the result)
Start Application Server
Open a terminal window and start the PicoLisp application server.
$ pil @lib/http.l @lib/xhtml.l @lib/form.l --server 8080 project.l +
A command prompt will not appear. The server is running and waiting for a connection. You can stop it later by pressing
Ctrl-C
in this terminal, or by running
killall pil
in another terminal window.
(In the following examples, we assume that this HTTP server continues to work)
Now open the URL '
http: // localhost: 8080 ' in the browser. You should see a blank page with a single line of text.
How it works?
The line above loads the debugger (option “+”), HTTP server code (“@ lib / http.l”), XHTML functions (“@ lib / xhtml.l”) and GUI-framework (“@ lib / form.l” , it will be required later for
interactive forms ).
The
server
function is then called with the default port number and URL. She will listen to this port for incoming HTTP requests in an endless loop. Whenever a GET request arrives on port 8080, the project.l file will be loaded and executed using
(load)During the execution of this file, all data written to the current output channel (STDOUT) is sent directly to the browser. The code in “project.l” is responsible for generating HTML (or another format that is understandable for the browser).
URL Syntax
The application server uses a slightly specialized syntax when exchanging URLs with the client. Part of the URL - “path” - what remains after trimming
- protocol, node and port specifications,
- final question mark and arguments
interpreted according to some rules. The most significant of them are:
- If the path begins with an exclamation point ('!'), The rest (without the '!') Is taken as the name of a Lisp function. All arguments after the question mark are passed to this function.
- If the path ends with a “.l” (full stop and “L”), it is taken as the name of a Lisp file, which is then loaded with (load) . This is the most common case, and we use it in our example “project.l”.
- If the file name extension matches an entry in the global mime
*Mimes
type table, the file is sent to the client with a mime-type and max-age value taken from this table. - Otherwise, the file is sent to the client with the mime type “application / octet-stream” and max-age in 1 second.
An application can expand or modify the
*Mimes
table with the
mime
function without restrictions. for example
(mime "doc" "application/msword" 60)
Defines a new mime type with max-age of one minute.
The argument values ​​in the URL, after the path and question mark, are encoded to preserve Lisp data types:
- Normal symbol (internal symbol) begins with a dollar symbol ("$")
- The number starts with a plus sign (+)
- External symbol (database object) begins with a dash ('-')
- The list (one-level only) is encoded with underscores ('_')
- Otherwise, it is a transit character (normal string).
Thus, high-level data types can be directly transferred to functions encoded in the URL, or assigned to global variables before loading the file.
Security
Of course, this is a huge security hole, if any Lisp file can be loaded right from the URL and any function can be called. For this reason, the application must take care to specify which files and functions are allowed in the URL. The server checks the global variable
* Allow , and when its value is not equal to
NIL
, denies access to what does not match its content.
As a rule,
*Allow
do not change directly, but use the functions
allowed and
allow (allowed ("app/") "!start" "@lib.css" "customer.l" "article.l" )
This is usually called at the beginning of the application and allows access to the app / directory, the start functions, and the @ lib.css, customer.l, and article.l files.
Later in the program,
*Allow
can be dynamically extended with
allow
(allow "!foo") (allow "newdir/" T)
This adds the function “foo” and the directory “newdir /” to the set of allowed elements.
File ".pw"
To be used in security monitoring (primarily to use the
psh
function, as in some later examples), you must create a file named “.pw” in the PicoLisp installation directory. This file must contain one line of arbitrary data for use as a password.
The recommended way to create this file is to call the
pw
function defined in “@ lib / http.l”
$ pil @lib/http.l -'pw 12' -bye
Please run this command.
html
function
Now back to our Hello World example. Basically, you could write “project.l” as a sequence of output statements.
######################################################################## (prinl "HTTP/1.0 200 OK^M") (prinl "Content-Type: text/html; charset=utf-8") (prinl "^M") (prinl "<html>") (prinl "Hello World!") (prinl "</html>") ########################################################################
but using the
html
function is much more convenient.
In addition,
html
is nothing more than a print function. You can easily see this if you connect the PicoLisp Shell (
psh
) to the server process (you had to generate a
.pw file for this) and enter the
html
function
$ /usr/lib/picolisp/bin/psh 8080 : (html 0 "Hello" "@lib.css" NIL "Hello World!") HTTP/1.0 200 OK Server: PicoLisp Date: Fri, 29 Dec 2006 07:28:58 GMT Cache-Control: max-age=0 Cache-Control: no-cache Content-Type: text/html
Arguments for
html
:
0
: The max-age value to control the browser cache (in seconds, zero means “no-cache”). You can set a higher value for pages that rarely change, or NIL
to turn off the cache control."Hello"
: the name of the page."@lib.css"
: the name of the CSS file. Pass NIL
if you do not want to use CSS, or a list of file names if you want to use more than one CSS file.NIL
: Specification of CSS attributes for the body
tag (see the description of CSS attributes below).
After these four arguments, an arbitrary number of expressions may follow. They form the body of the page and are calculated according to special rules.
These rules are slightly different from the usual rules for computing:
- If the argument is an atom (number or character / string), its value will be printed immediately.
- Otherwise (list), it is calculated as Lisp functions (usually these are some forms of the print operator).
Thus, our source file can also be written as:
######################################################################## (html 0 "Hello" "@lib.css" NIL (prinl "Hello World!") ) ########################################################################
The most common printing features are some HTML tags:
######################################################################## (html 0 "Hello" "@lib.css" NIL (<h1> NIL "Hello World!") (<br> "This is some text.") (ht:Prin "And this is a number: " (+ 1 2 3)) ) ########################################################################
<h1>
and
<br>
are tag functions.
<h1>
takes the CSS attribute as its first argument.
Note the use of
ht:Prin
instead of
prin
. The
ht:Prin
function should be used to directly print in HTML pages, because it escapes special characters.
CSS Attributes
The html
function is above and many of the
tag functions accept CSS attribute specifications. This can be an atom, cons pairs, or a list of cons pairs. Let's show it on the example of the
<h1>
.
An atom (usually a character or a string) is taken as the name of a CSS class
: (<h1> 'foo "Title") <h1 class="foo">Title</h1>
For a cons pair, CAR is taken as attribute name and CDR as attribute value.
: (<h1> '(id . bar) "Title") <h1 id="bar">Title</h1>
Hence the list of cons pairs gives a set of attribute-value pairs.
: (<h1> '((id . "abc") (lang . "de")) "Title") <h1 id="abc" lang="de">Title</h1>
Tag Functions
All the predefined functions of XHTML tags can be found in “@ lib / xhtml.l”. We recommend to take a look at their definitions and experiment a bit by executing them in the PicoLisp console, or by clicking the Refresh button of the browser after editing the project.l file.
To get a suitable PicoLisp console, execute the PicoLisp Shell (
psh
) command in a separate terminal window (works only if the application server is running and you have created the
“.pw” file )
$ /usr/lib/picolisp/bin/psh 8080 :
or just run the interpreter with the loading of the "@ lib / xhtml.l" module
$ pil @lib/http.l @lib/xhtml.l + :
Note that for all these tag functions, the
rules for calculating tag functions apply.
Simple Tags
Most of the tag functions are simple and straightforward. Some of them just print their arguments.
: (<br> "Hello world") Hello world<br/> : (<em> "Hello world") <em>Hello world</em>
However, most of them accept a
set of CSS attributes as the first argument (as the
<h1>
above)
: (<div> 'main "Hello world") <div class="main">Hello world</div> : (<p> NIL "Hello world") <p>Hello world</p> : (<p> 'info "Hello world") <p class="info">Hello world</p>
All these functions take an arbitrary number of arguments and can have an arbitrary depth of nesting (as long as the resulting HTML markup remains valid)
: (<div> 'main (<h1> NIL "Head") (<p> NIL (<br> "Line 1") "Line" (<nbsp>) (+ 1 1) ) ) <div class="main"><h1>Head</h1> <p>Line 1<br/> Line 2</p> </div>
Lists
HTML lists implemented by the
<ol>
and
<ul>
tags allow you to define hierarchical structures. Paste the following code into your copy of “project.l”:
######################################################################## (html 0 "Unordered List" "@lib.css" NIL (<ul> NIL (<li> NIL "Item 1") (<li> NIL "Sublist 1" (<ul> NIL (<li> NIL "Item 1-1") (<li> NIL "Item 1-2") ) ) (<li> NIL "Item 2") (<li> NIL "Sublist 2" (<ul> NIL (<li> NIL "Item 2-1") (<li> NIL "Item 2-2") ) ) (<li> NIL "Item 3") ) ) ########################################################################
Here you can also place arbitrary code in every node of this tree, including other tag functions.
Tables
Like hierarchical structures, you can generate two-dimensional tables using the
<table>
and
<row>
functions.
The following example displays a table of numbers and their squares:
######################################################################## (html 0 "Table" "@lib.css" NIL (<table> NIL NIL NIL (for N 10 # A table with 10 rows (<row> NIL N (prin (* NN))) ) ) ) # and 2 columns ########################################################################
The first
<table>
argument is a regular CSS attribute, the second is an optional header, and the third is an optional list of column headers. In this list, you can transfer the list for each column, from the CSS attribute to CAR, and the body of the tag to the CDR for the contents of the column header.
The
<table>
body contains calls to the
<row>
function. The feature of the function is that each expression in its body will go to a separate column of the table. If a CSS attribute is specified for the column header and the
<row>
function, they will be combined with a space and passed to the
<td>
. This allows you to specify different CSS attributes for each row and column.
As an extension of the example table above, let's set some attributes for the table itself (although it’s not recommended to do this — it’s better to define such styles in the CSS file and then just pass the class name to
<table>
), align both columns to the right and output each string to variable (red and blue) colors
######################################################################## (html 0 "Table" "@lib.css" NIL (<table> '((width . "200px") (style . "border: dotted 1px;")) # table style "Square Numbers" # caption '((align "Number") (align "Square")) # 2 headers (for N 10 # 10 rows (<row> (xchg '(red) '(blue)) # red or blue N # 2 columns (prin (* NN) ) ) ) ) ) ########################################################################
If you want to combine two or more cells in a table so that one cell occupies several columns, you can pass the “
-
” symbol as an additional
<row>
parameter. This will cause the data to the left of the "
-
" characters to be expanded to the right.
You can also directly define the table structure with the usual tag functions,
<th>
,
<tr>
and
<td>
.
If you just need a two-dimensional arrangement of components, you can use the even simpler
<grid>
function:
######################################################################## (html 0 "Grid" "@lib.css" NIL (<grid> 3 "A" "B" "C" 123 456 789 ) ) ########################################################################
The first parameter is the number of columns (here: 3), and then one expression for each cell. Instead of a number, you can also pass in a list of CSS attributes. The length of this list will determine the number of columns. You can change the second line in the above example to:
(<grid> '(NIL NIL right)
At the same time the third column will be aligned to the right.
Menus and Tabs (Tabs)
The two most powerful tag functions are
<menu>
and
<tab>
. When used alone or in combination, they form a navigation framework with
- menu items where you can open and close submenus
- submenu items to navigate to different pages
- tabs (tabs), to go to different sub-pages
The following example is not very useful, because the URLs of all elements lead to the same page “project.l”, but this is enough to demonstrate the functionality:
######################################################################## (html 0 "Menu+Tab" "@lib.css" NIL (<div> '(id . menu) (<menu> ("Item" "project.l") # Top level item (NIL (<hr>)) # Plain HTML (T "Submenu 1" # Submenu ("Subitem 1.1" "project.l") (T "Submenu 1.2" ("Subitem 1.2.1" "project.l") ("Subitem 1.2.2" "project.l") ("Subitem 1.2.3" "project.l") ) ("Subitem 1.3" "project.l") ) (T "Submenu 2" ("Subitem 2.1" "project.l") ("Subitem 2.2" "project.l") ) ) ) (<div> '(id . main) (<h1> NIL "Menu+Tab") (<tab> ("Tab1" (<h3> NIL "This is Tab 1") ) ("Tab2" (<h3> NIL "This is Tab 2") ) ("Tab3" (<h3> NIL "This is Tab 3") ) ) ) ) ########################################################################
<menu>
accepts a sequence of menu items. Each menu item is a Lisp list with CAR:
NIL
: inactive menu item, the rest of the list may consist of arbitrary code (usually HTML tags).T
: The second element is the name of the submenu, and clicking on this name will open or close the corresponding submenu. The tail of the list recursively defines a submenu (arbitrary depth of nesting).- Otherwise: The menu item defines a direct action (instead of opening a submenu), where the first item in the list is the name of the menu item and the second item is the corresponding URL.
<tab>
takes a list of subpages. Each page is just a tab name, and then arbitrary code (usually HTML tags).
Please note that only one menu and one tab can be active at one time.
Interactive forms
In HTML, the only way for user input is through the
<form>
and
<input>
elements, using the HTTP POST method to communicate with the server.
"@ Lib / xhtml.l" defines a function called
<post>
and a collection of data entry tags that allow direct programming of HTML forms. We will show only one simple example:
######################################################################## (html 0 "Simple Form" "@lib.css" NIL (<post> NIL "project.l" (<field> 10 '*Text) (<submit> "Save") ) ) ########################################################################
This example links the text input field with a global variable
*Text
. The field displays the current value of
*Text
, and pressing the submit button causes a reload of “project.l” with
*Text
, which is assigned a string value entered by the user.
An application can then use this variable for some useful actions, such as storing its value in a database.
The problem with this straightforward use of forms is that:
- they require the programmer to take care of maintaining a large number of global variables. Each input field on the page requires a variable for communication between the server and the client.
- they do not preserve the internal state of the application. Each POST request generates a separate process on the server, which sets global variables to their new values, generates HTML pages and ends thereafter. The state of the application must be transmitted explicitly, for example using the
<hidden>
. - they are not very interactive. Usually they have only one “Send” button. The user fills (possibly) a large number of input fields, but the changes will take effect only when you click the “Submit” button.
Although we wrote several applications in this style, we recommend the GUI framework provided by "@ lib / form.l". It does not need any variables for client-server interaction, but implements a hierarchy of classes of GUI components for abstracting application logic, button actions, and data communication.
Sessions
First of all, we need to create a permanent environment on the server to handle each separate session (for each connected client).
Technically, this is just a child server process that we launched
above , which
does not terminate immediately after it sends an HTML page to the browser. This is achieved by calling the
app
function somewhere in the application initialization code.
######################################################################## (app) # Start a session (html 0 "Simple Session" "@lib.css" NIL (<post> NIL "project.l" (<field> 10 '*Text) (<submit> "Save") ) ) ########################################################################
No more differences from the previous example. However, when you connect your browser and then look at the terminal window where the application server is running, you will notice a colon - the PicoLisp command line
$ pil @lib/http.l @lib/xhtml.l @lib/form.l --server 8080 project.l + :
A tool like the Unix
ps
utility will tell you that two
picolisp
processes are now running, the first of which is the parent of the second.
If you enter some text, say “abcdef”, in the text field in the browser window, and click the Save button and examine the
*Text
variable,
: *Text -> "abcdef"
You will see that we now have a dedicated PicoLisp process “connected” to the client.
You can complete this process (like any PicoLisp interactive session) by pressing
Ctrl-D
on an empty command prompt. Otherwise, it will finish itself, if other requests from the browser do not arrive within the waiting time (by default, 5 minutes).
To start the sales version (without debugging), the server is usually started without the “+” flag and with
-wait
$ pil @lib/http.l @lib/xhtml.l @lib/form.l --server 8080 project.l -wait
With this launch option, the command line prompt (:) does not appear when the client connects.
Forms (action forms)
Now that we have a session for each client, we can set up an active GUI framework.
To do this, we wrap the
html
function call in
action
.
Inside the body, html
besides all other types of tag functions, there can be one or several calls toform
######################################################################## (app) # Start session (action # Action handler (html 0 "Form" "@lib.css" NIL # HTTP/HTML protocol (form NIL # Form (gui 'a '(+TextField) 10) # Text Field (gui '(+Button) "Print" # Button '(msg (val> (: home a))) ) ) ) ) ########################################################################
Note that there is no longer a global variable, such as *Text
, to store the contents of the input field. Instead, we gave the local, symbolic name a
"component"+TextField
(gui 'a '(+TextField) 10) # Text Field
Other components can access it. '(msg (val> (: home a)))
(: home)
always returns a link to the form that contains this GUI component. So (: home a)
- the link to the component ' a
' in the current form. Since msg outputs its argument to STDERR, and the method val>
retrieves the current contents of the component, when we click the button, we will see the text entered in the text field in the console.Individually action
and form
do not have much meaning. However, inside html
and out form
, you can freely mix HTML functions (and any other Lisp functions).A typical page might have this structure: (action # Action handler (html .. # HTTP/HTML protocol (<h1> ..) # HTML tags (form NIL # Form (<h3> ..) (gui ..) # GUI component(s) (gui ..) .. ) (<h2> ..) (form NIL # Another form (<h3> ..) (gui ..) # GUI component(s) .. ) (<br> ..) .. ) )
Function gui
The most significant function in the body form
is the function gui
. This is a workhorse of building a graphical interface.Outside the function is form
gui
not defined. The first parameter is an optional alias, a list of classes and additional arguments necessary for the constructors of these classes. We saw in the previous example (gui 'a '(+TextField) 10) # Text Field
Here ' a
' is the alias for the type component (+TextField)
. The numeric argument 10
is passed to the text field, specifying its width. See other examples in the GUI Classes chapter .During a GET request gui
, essentially a front end for new
. It builds the component, saves it to the internal structures of the current form, and initializes it by sending a message to the component init>
. Finally, it sends the message show>
to generate the HTML code and pass it to the browser.During the POST request gui
does not build any new components. Instead, existing components are reused. Therefore, gui
there is no need to do anything more than sending a message component show>
.Control flow
HTTP has only two methods for changing the browser window: GET and POST. We use these two methods in a well-defined way:- GET means that a new page is being created . It is used when the page is visited for the first time, usually by entering the URL in the browser’s address field, or by clicking on a link (which is often a submenu or tab ).
- POST always leads to the same page . It is initiated by pressing a button on the form, updates the data structures of the corresponding form and executes the code associated with this button.
The code associated with the button can do almost everything: read and change the contents of the input field, communicate with the database, display messages and dialog boxes, or even forge a POST request with a GET request, which will result in a completely different document being shown (see Change URL addresses ).GET builds all the GUI components on the server. These components are objects that encapsulate the state and behavior of an HTML page in a browser. Whenever the button is pressed, the page is reloaded using a POST request. Then - before sending any data to the browser - control is transferred to the function action
. It performs error checking on all components, handles user input on the HTML page, and stores the values ​​in each component in the correct format (text, number, date, object, etc.).The state of the form persists over time. When the user returns to the previous page with the browser's Back button, this state is resumed and can be sent again (by POST request).The following simple example displays two text fields. If you enter some text in the “Source” field, you can copy it in upper or lower case into the “Destination” field by pressing one of the buttons. ######################################################################## (app) (action (html 0 "Case Conversion" "@lib.css" NIL (form NIL (<grid> 2 "Source" (gui 'src '(+TextField) 30) "Destination" (gui 'dst '(+Lock +TextField) 30) ) (gui '(+JS +Button) "Upper Case" '(set> (: home dst) (uppc (val> (: home src))) ) ) (gui '(+JS +Button) "Lower Case" '(set> (: home dst) (lowc (val> (: home src))) ) ) ) ) ) ########################################################################
The prefix class +Lock
in the “Destination” field makes this field read-only. The only way to put some text in this field is to use one of the buttons.URL switching
Since the action code (buttons) is executed before it html
can send an HTTP header, it can interrupt the current page and submit something else to the user. This may be another HTML page, but this is not a very interesting case, since a normal link would be enough. Instead, the code can trigger the loading of dynamically generated data.The following example shows a text box and two buttons. Any text entered in the text area is exported to a text file via the first button or a PDF document through the second button. ######################################################################## (load "@lib/ps.l") (app) (action (html 0 "Export" "@lib.css" NIL (form NIL (gui '(+TextField) 30 8) (gui '(+Button) "Text" '(let Txt (tmp "export.txt") (out Txt (prinl (val> (: home gui 1)))) (url Txt) ) ) (gui '(+Button) "PDF" '(psOut NIL "foo" (a4) (indent 40 40) (down 60) (hline 3) (font (14 . "Times-Roman") (ps (val> (: home gui 1))) ) (hline 3) (page) ) ) ) ) ) ########################################################################
(when you pass two arguments to a class +TextField
(width and height), an element is created textarea
)The action code of the first button creates a temporary file (that is, a file called "export.txt" in the temporary directory of the current process), prints the value of the text area (this time we do not give the name of the gui-element, but simply refer to it as the first element in the form in this file, and then the function url
with the file name is called.The second button uses the “@ lib / ps.l” PostScript library to create a temporary file “foo.pdf”. Here the creation of a temporary file and the function call are url
hidden inside psOut
. As a result, the browser receives the PDF document and displays it.Alerts and Conversations
In fact, alerts and dialogs are not exactly what we are used to seeing ;-)They are not popup. In this framework, they are kind of easy-to-use forms of a predefined type. They can be called from the button code, and they always appear on the current page, immediately before the form that created them.Let's look at an example that uses two alerts and a dialog box. At the beginning, it displays a simple form with a locked text field and two buttons. ######################################################################## (app) (action (html 0 "Alerts and Dialogs" "@lib.css" NIL (form NIL (gui '(+Init +Lock +TextField) "Initial Text" 20 "My Text") (gui '(+Button) "Alert" '(alert NIL "This is an alert " (okButton)) ) (gui '(+Button) "Dialog" '(dialog NIL (<br> "This is a dialog.") (<br> "You can change the text here " (gui '(+Init +TextField) (val> (: top 1 gui 1)) 20) ) (<br> "and then re-submit it to the form.") (gui '(+Button) "Re-Submit" '(alert NIL "Are you sure? " (yesButton '(set> (: home top 2 gui 1) (val> (: home top 1 gui 1)) ) ) (noButton) ) ) (cancelButton) ) ) ) ) ) ########################################################################
The prefix class +Init
initializes the “My Text” field with the string “Initial Text”. Since the field is disabled, this value cannot be changed directly.The first button will show the alert: "This is an alert". You can close it by clicking "OK".The second button shows a dialog with a text field containing a copy of the value from the text field of the main form. You can change this value and send it back to the form if you click “Re-Submit” and answer “Yes” to the warning “Are you sure?”.Calculator Example
Now we will briefly postpone our test file “project.l” and move on to a more substantial and practical autonomous example. Using what we have learned, we will build a simple bignum-calculator. (“Bignum” because there is only one type of number in PicoLisp - bignums (unlimited whole numbers))It uses one form, one numeric input field and many buttons. The source code of the calculator can be found in the PicoLisp distribution (for example, in “/usr/share/picolisp/misc/calc.l”) along with the directly executed “misc / calc” wrapper script.calc.l # 14may11abu # (c) Software Lab. Alexander Burger # *Init *Accu *Stack (allowed NIL "!calculator" "@lib.css") (load "@lib/http.l" "@lib/xhtml.l" "@lib/form.l") # Calculator logic (de digit (N) (when *Init (zero *Accu) (off *Init)) (setq *Accu (+ N (* 10 *Accu))) ) (de calc () (let (Fun (caar *Stack) Val (cddr (pop '*Stack))) (setq *Accu (if (and (== '/ Fun) (=0 *Accu)) (alert "Div / 0") (Fun Val *Accu) ) ) ) ) (de operand (Fun Prio) (when (>= (cadar *Stack) Prio) (calc)) (push '*Stack (cons Fun Prio *Accu)) (on *Init) ) (de finish () (while *Stack (calc)) (on *Init) ) # Calculator GUI (de calculator () (app) (action (html 0 "Bignum Calculator" "@lib.css" NIL (<h2> NIL "Bignum Calculator") (form NIL (<br> (gui '(+Var +NumField) '*Accu 60)) (<grid> 4 (gui '(+JS +Button) "±" '(setq *Accu (- *Accu))) (gui '(+Able +JS +Button) '(ge0 *Accu) (char 8730) '(setq *Accu (sqrt *Accu)) ) (gui '(+JS +Button) "\^" '(operand '** 3)) (gui '(+JS +Button) "/" '(operand '/ 2)) (gui '(+JS +Button) "7" '(digit 7)) (gui '(+JS +Button) "8" '(digit 8)) (gui '(+JS +Button) "9" '(digit 9)) (gui '(+JS +Button) "*" '(operand '* 2)) (gui '(+JS +Button) "4" '(digit 4)) (gui '(+JS +Button) "5" '(digit 5)) (gui '(+JS +Button) "6" '(digit 6)) (gui '(+JS +Button) "-" '(operand '- 1)) (gui '(+JS +Button) "1" '(digit 1)) (gui '(+JS +Button) "2" '(digit 2)) (gui '(+JS +Button) "3" '(digit 3)) (gui '(+JS +Button) "+" '(operand '+ 1)) (gui '(+JS +Button) "0" '(digit 0)) (gui '(+JS +Button) "C" '(zero *Accu)) (gui '(+JS +Button) "A" '(main)) (gui '(+JS +Button) "=" '(finish)) ) ) ) ) ) # Initialize (de main () (on *Init) (zero *Accu) (off *Stack) ) # Start server (de go () (server 8080 "!calculator") )
To use, go to the PicoLisp installation directory and run it as $ misc/calc
or call it with an absolute path, for example $ /usr/share/picolisp/misc/calc
If you want to access the PicoLisp interactive session, run it instead as $ pil misc/calc.l -main -go +
Open as usual the page ' http: // localhost: 8080 '.The code for calculator logic and GUI is quite simple. The entry point is the function calculator
. It is called directly (as described in the URL Syntax ), first, as the default URL of the server, and second, implicitly through POST requests. After running the calculator, file access is not required.Note that for the production version we insert the allowed statement at the beginning of “misc / calc.l” (as recommended in the Security chapter ) (allowed NIL "!calculator" "@lib.css")
This will limit external access to a single function calculator
.Calculator uses three global variables *Init
, *Accu
and *Stack
. *Init
- a boolean flag set by operator buttons to indicate that the next pressed digit will reset the battery to zero. *Accu
- accumulator (accumulator). Its value is displayed in a numeric input field, accepts user input and stores the results of calculations. *Stack
represents a store stack for storing deferred calculations (operators, priorities and intermediate results) with lower priority operators, while calculations with higher priority are performed.The function digit
is called with the number buttons and adds another digit to the battery.The function calc
takes the step of the actual calculation. It retrieves data from the stack, checks for division by zero, and displays an error message if necessary.operand
handles operand buttons, taking function and priority as arguments. It compares the priority with the value at the top of the stack and postpones the calculation if it is smaller.finish
used to calculate the final result.The function has calculator
one numeric input field, 60 characters wide. (gui '(+Var +NumField) '*Accu 60)
The prefix class +Var
associates this field with a global variable *Accu
. All changes in the field will be displayed in this variable, and the change in the value of this variable will be displayed in the field.The square root button has a prefix class +Able
(gui '(+Able +JS +Button) '(ge0 *Accu) (char 8730) '(setq *Accu (sqrt *Accu)) )
with an argument in the form of an expression that checks that the current value in the battery is greater than zero, otherwise the button is disabled.The rest of the form is just an array (GRID) of buttons that encapsulates all the functions of the calculator. The user can enter numbers in the input field using the numeric buttons or directly typing them, and perform calculations with the operator buttons. Supported operations are addition, subtraction, multiplication, division, sign reversal, square root, and exponentiation (all operations are in long integer arithmetic). The ' C
' button clears only the battery, while the ' A
' button also clears all pending calculations.And all this in 53 lines of code!
Tables (charts)
Charts - virtual components for the internal presentation of tabular data.As a rule, this data is nested lists, database selections or dynamically generated tabular information. Charts allow you to view data in rows and columns (usually in HTML tables ), scroll up and down, and link them with the corresponding visible GUI components.In fact, the logic for handling Charts constitutes a significant part of the entire framework with a large influence on all internal mechanisms. Each GUI component must know whether it is part of a Chart in order to be able to correctly process its contents during an update and user interaction.Suppose we want to collect text and numeric data. We could create a table ######################################################################## (app) (action (html 0 "Table" "@lib.css" NIL (form NIL (<table> NIL NIL '((NIL "Text") (NIL "Number")) (do 4 (<row> NIL (gui '(+TextField) 20) (gui '(+NumField) 10) ) ) ) (<submit> "Save") ) ) ) ########################################################################
with two columns "Text" and "Number" and four lines, each of which contains +TextField
and +NumField
.You can enter text in the first column and numbers in the second. Pressing the “Save” button saves these values ​​to components on the server (or gives an error message if the row in the second column is not a number).There are two problems with this solution:- Although you can get the entered data for individual fields, for example
: (val> (get *Top 'gui 2)) # Value in the first row, second column -> 123
There is no direct way to get the entire data structure in a single list. Instead, you have to bypass all the GUI components and collect data. - The user cannot enter more than four lines of data, because there is no easy way to scroll down and expand the space to enter more data.
Chart can handle these things: ######################################################################## (app) (action (html 0 "Chart" "@lib.css" NIL (form NIL (gui '(+Chart) 2) # Inserted a +Chart (<table> NIL NIL '((NIL "Text") (NIL "Number")) (do 4 (<row> NIL (gui 1 '(+TextField) 20) # Inserted '1' (gui 2 '(+NumField) 10) ) ) ) # Inserted '2' (<submit> "Save") ) ) ) ########################################################################
Notice that we inserted the component +Chart
in front of the GUI components that need to be managed using the Chart. Argument "2" means that Chart should expect two columns of data.Each component received an index number (here "1" and "2") as the first argument gui
, indicating the column in which this component should go inside the Chart.Now - if you entered "a", "b" and "c" in the first, and 1, 2 and 3 in the second column - we can get the full contents of the table by sending a messageval>
: (val> (get *Top 'chart 1)) # Retrieve the value of the first chart -> (("a" 1) ("b" 2) ("c" 3))
By the way, a more convenient function is chart
: (val> (chart)) # Retrieve the value of the current chart -> (("a" 1) ("b" 2) ("c" 3))
chart
can be used instead of the above construction, when we want to access the “current” table, that is, the last one recently used in the current form.Scroll
To enable scrolling, let's add two buttons. We use predefined classes +UpButton
and+DnButton
######################################################################## (app) (action (html 0 "Scrollable Chart" "@lib.css" NIL (form NIL (gui '(+Chart) 2) (<table> NIL NIL '((NIL "Text") (NIL "Number")) (do 4 (<row> NIL (gui 1 '(+TextField) 20) (gui 2 '(+NumField) 10) ) ) ) (gui '(+UpButton) 1) # Inserted two buttons (gui '(+DnButton) 1) (----) (<submit> "Save") ) ) ) ########################################################################
to scroll up and down one line at a time (argument “1”).Now you can enter a few rows of data, scroll down and enter more data. There is no need to press the “Save” button (except for the very beginning when the scroll buttons are disabled), because any button in the form sends the changes to the server before performing any actions.Get and Put functions
As we said, Chart is a virtual component for editing tabular data. Thus, Chart's own data format is a list of lists: each sublist represents one row of data, and each element of the row corresponds to one GUI component.In the example above, we saw a row, such as ("a" 1)
compared (gui 1 '(+TextField) 20) (gui 2 '(+NumField) 10)
However, quite often such a one-to-one relationship is not desirable. It may be necessary to present the user with internal data structures in various forms, and user input may require conversion to an internal format.For this, the Chart accepts — in addition to the “number of columns” argument — two additional functional arguments. The first function is called to “put” the internal representation into the GUI components, and the second to “get” (receive) the data from the GUI into the internal representation.A typical example is a table for displaying clients in a database. While the internal representation is a one-dimensional list of customer objects, “put” expands each object into the list: last name, first name, telephone number, address, and so on. When the user enters the client name, “get” finds the corresponding object in the database and stores it in an internal representation. In turn, “put” will deploy it to the GUI.Now, let's look at a simpler example: Chart, which contains only a list of numbers, but also displays the text form of each number in the GUI (in German). ######################################################################## (app) (load "@lib/zahlwort.l") (action (html 0 "Numerals" "@lib.css" NIL (form NIL (gui '(+Init +Chart) (1 5 7) 2 '((N) (list N (zahlwort N))) car ) (<table> NIL NIL '((NIL "Numeral") (NIL "German")) (do 4 (<row> NIL (gui 1 '(+NumField) 9) (gui 2 '(+Lock +TextField) 90) ) ) ) (gui '(+UpButton) 1) (gui '(+DnButton) 1) (----) (<submit> "Save") ) ) ) ########################################################################
"@ Lib / zahlwort.l" defines a helper function zahlwort
, which will later come in handy in the 'put' function. zahlwort
takes a number and returns its name in German.Now look at the code (gui '(+Init +Chart) (1 5 7) 2 '((N) (list N (zahlwort N))) car )
We add the prefix class +Init
to +Chart
and pass it a list (1 5 7)
for the initial value of the chart. Then, after '2' (the chart has two columns), we pass the “put” function: '((N) (list N (zahlwort N)))
which takes a number and returns a list of that number and its name, and the get function car )
which in turn accepts such a list and returns a number that will just be the first element of the list.You can see from this example that “get” is the inverse of the “put” function. "Get" can be omitted if the chart is read-only (does not contain (or contains only blocked) input fields).Field in the second column (gui 2 '(+Lock +TextField) 90) ) ) )
blocked because it displays the text created by the “put” function and does not intend to accept user input.When you open this form in a browser, you will see three prefilled lines with “1 / eins”, “5 / fünf” and “7 / sieben”, according to the (1 5 7)
class arguments +Init
. By entering a number in the first column and pressing the ENTER key or one of the buttons, you will see the corresponding text in the second column.
GUI Classes
In previous chapters, we have seen examples of GUI classes, such as +TextField
, +NumField
or +Button
, often in combination with prefix classes +Lock
, +Init
or +Able
. Now we take a wider look at the entire hierarchy and try more examples.The abstract class +gui
is the basis of all GUI classes. You can see class hierarchies live using the dep function (“dependencies”):gui class hierarchy : (dep '+gui) +gui +JsField +Button +UpButton +PickButton +DstButton +ClrButton +ChoButton +Choice +GoButton +BubbleButton +DelRowButton +ShowButton +DnButton +Img +field +Checkbox +TextField +FileField +ClassField +numField +NumField +FixField +BlobField +DateField +SymField +UpField +MailField +SexField +AtomField +PwField +ListTextField +LinesField +TelField +TimeField +HttpField +Radio -> +gui
For example, we see what +DnButton
is a subclass +Button
, which in turn is a subclass +gui
. Inspecting directly+DnButton
: (dep '+DnButton) +Tiny +Rid +JS +Able +gui +Button +DnButton -> +DnButton
We see that +DnButton
inherits from +Tiny
, +Rid
, +Able
and +Button
. The actual definition +DnButton
can be found in "@ lib / form.l" (class +DnButton +Tiny +Rid +JS +Able +Button) ...
In general, “@ lib / form.l” is a comprehensive reference for the GUI framework and is recommended for familiarization.
Input fields
Input fields implement a visual display of application data and allow, when enabled, the input and modification of this data.At the HTML level, they can take the following forms:- Regular Input Fields
- Textarea
- checkboxes
- drop-down lists (combobox)
- Password fields
- HTML links
- Plain html text
With the exception of checkboxes that are implemented class Checkbox , all these HTML-generated representation +TextField
and its respective subclasses, such as +NumField
, +DateField
etc. Their actual appearance (as one of the above forms) depends on their arguments:We have already seen “normal” text fields. They are created by one numeric argument. This example creates a variable field with a width of 10 characters: (gui '(+TextField) 10)
If the second numeric argument is specified for the number of lines (in this case, '4'), you will get a textarea: (gui '(+TextField) 10 4)
Providing a list of values ​​instead of a number gives the combobox: (gui '(+TextField) '("Value 1" "Value 2" "Value 3"))
In addition to these arguments, you can pass a string. The field will be labeled: (gui '(+TextField) 10 "Plain") (gui '(+TextField) 10 4 "Text Area") (gui '(+TextField) '("Value 1" "Value 2" "Value 3") "Selection")
Finally, without arguments, the field will be displayed as plain HTML text: (gui '(+TextField))
This mainly makes sense in combination with type prefix classes +Var
and +Obj
, for managing the contents of these fields, and for special behavior, such as HTML links or scrollable table values.Numeric input fields
+NumField
returns a number in its method val>
and takes a number in the method set>
. It gives an error message when user input cannot be converted to a number.Large numbers are displayed with thousands separators as defined in the current locale. ######################################################################## (app) (action (html 0 "+NumField" "@lib.css" NIL (form NIL (gui '(+NumField) 10) (gui '(+JS +Button) "Print value" '(msg (val> (: home gui 1))) ) (gui '(+JS +Button) "Set to 123" '(set> (: home gui 1) 123) ) ) ) ) ########################################################################
+FixField
requires an additional scale-factor argument, and accepts / returns scaled numbers with a fixed comma.The decimal separator is determined by the current locale. ######################################################################## (app) (action (html 0 "+FixField" "@lib.css" NIL (form NIL (gui '(+FixField) 3 10) (gui '(+JS +Button) "Print value" '(msg (format (val> (: home gui 1)) 3)) ) (gui '(+JS +Button) "Set to 123.456" '(set> (: home gui 1) 123456) ) ) ) ) ########################################################################
Time and Date
+DateField
accepts and returns date values . ######################################################################## (app) (action (html 0 "+DateField" "@lib.css" NIL (form NIL (gui '(+DateField) 10) (gui '(+JS +Button) "Print value" '(msg (datStr (val> (: home gui 1)))) ) (gui '(+JS +Button) "Set to \"today\"" '(set> (: home gui 1) (date)) ) ) ) ) ########################################################################
The format for displaying and entering the date depends on the current locale (see datStr and expDat ). You can change the locale, for example : (locale "DE" "de") -> NIL
If no locale is selected, then the default format is YYYY-MM-DD. Some predefined locales use the DD.MM.YYYY (DE), YYYY / MM / DD (JP), DD / MM / YYYY (UK), or MM / DD / YYYY (US) patterns.When the user input does not match the date format of the current locale, an error is generated.Regardless of locale, +DateField
trying to expand reduced input from the user:- "7" gives the 7th of the current month
- "031" or "0301" gives January 3 of the current year
- "311" or "3101" gives January 31 of the current year
- "0311" gives November 3 of the current year
- “01023” or “010203” are given on February 1, 2003
- and so on
Similar class - +TimeField
. It accepts and returns the time value . ######################################################################## (app) (action (html 0 "+TimeField" "@lib.css" NIL (form NIL (gui '(+TimeField) 8) (gui '(+JS +Button) "Print value" '(msg (tim$ (val> (: home gui 1)))) ) (gui '(+JS +Button) "Set to \"now\"" '(set> (: home gui 1) (time)) ) ) ) ) ########################################################################
When the field width is "8", as in this example, the time is displayed in a format HH:MM:SS
. Another possible value is “5”, causes the +TimeField
value to be displayed as HH:MM
.When user input cannot be converted to a time value, an error is generated.The user can omit the colon. Incomplete input is converted, just like the date. “125” is converted to “12:05”, “124517” to “12:45:17”, and so on.Phone numbers
Internal representation of telephone numbers: country code (without leading signs plus or zero), then local telephone number (ideally separated by spaces) and additional number (ideally separated by a hyphen). The exact format of the phone number is not imposed by the GUI, but for further processing (for example, searching the database), you usually use fold for better reproducibility.To display a phone number, +TelField
replace the country code with a zero if it is a country code from the current locale, or is preceded by a plus sign, if it is a foreign country code (see telStr ).For the number entered by the user, the plus or double zero sign is simply discarded, while one leading zero is replaced with the country code of the current locale (see expTel ). ######################################################################## (app) (locale "DE" "de") (action (html 0 "+TelField" "@lib.css" NIL (form NIL (gui '(+TelField) 20) (gui '(+JS +Button) "Print value" '(msg (val> (: home gui 1))) ) (gui '(+JS +Button) "Set to \"49 1234 5678-0\"" '(set> (: home gui 1) "49 1234 5678-0") ) ) ) ) ########################################################################
Checkboxes
The class +Checkbox
is simple. User interaction is limited to turning it on and off. It takes a boolean value ( NIL
or non- NIL
) and returns T
or NIL
. ######################################################################## (app) (action (html 0 "+Checkbox" "@lib.css" NIL (form NIL (gui '(+Checkbox)) (gui '(+JS +Button) "Print value" '(msg (val> (: home gui 1))) ) (gui '(+JS +Button) "On" '(set> (: home gui 1) T) ) (gui '(+JS +Button) "Off" '(set> (: home gui 1) NIL) ) ) ) ) ########################################################################
Field prefix classes
Most of the capabilities of the framework are explained by the combinatorial flexibility of the class prefix for GUI and DB objects. They allow you to 'redefine' individual methods in the inheritance tree 'in an operational way' and can be combined in various ways to achieve any desired behavior.Technically, there is nothing special about prefix classes. These are regular classes. They are called "prefix" because they are intended to be inserted in front of other classes in the list of superclasses of an object or class.Usually they take their own arguments for their method T
from the list of arguments to the function gui
(the method T
is the constructor (comment of the translator).Initialization
+Init
overrides the method init>
for this component. The message is init>
sent to the +gui
component when the page is loaded for the first time (during a GET request). +Init
accepts an expression for the initial value of this field. (gui '(+Init +TextField) "This is the initial text" 30)
Other classes that automatically set the value of the field are +Var
(linking the field to a variable) and +E/R
(linking the field to the database).+Cue
can be used, for example, in the "required" fields to prompt the user about what he should enter. It will display the value of the argument in angle brackets, only if the field value is equal NIL
and the method val>
returns NIL
, despite the fact that this value will be displayed.Call the empty field to display "<Enter text here>": (gui '(+Cue +TextField) "Please enter some text here" 30)
Enable and disable
An important feature of the GUI is context-sensitive disabling and enabling individual components, or the entire form.+Able
the class prefix takes an expression argument and disables the component if that expression evaluates to NIL
. We have seen an example of its use in the square root button of a calculator. Or imagine a button that should be turned on only after Christmas. (gui '(+Able +Button) '(>= (cdr (date (date))) (12 24)) "Close this year" '(endOfYearProcessing) )
or password entry field, which is disabled after logging in (gui '(+Able +PwField) '(not *Login) 10 "Password")
A special case is the prefix +Lock
, which unconditionally disables the component. He takes no arguments (gui '(+Lock +NumField) 10 "Count")
("10" and "Count" are arguments for +NumField
) and creates a read-only field.The entire form can be disabled by calling disable
with an NIL
argument. This affects all components in this form. Using the example above, we can make the form read-only before Christmas. (form NIL (disable (> (12 24) (cdr (date (date))))) # Disable whole form (gui ..) .. )
However, even in a completely locked form, it is often necessary to include certain components, since they are necessary for navigation, scrolling, or other actions that do not affect the contents of the form. This is done by setting the prefix +Rid
. (form NIL (disable (> (12 24) (cdr (date (date))))) (gui ..) .. (gui '(+Rid +Button) ..) # Button is enabled despite the disabled form .. )
Formatting
GUI prefix classes allow you to fine-tune how values ​​are stored and retrieved from components. Like the predefined classes of the type +NumField
or +DateField
, they override the methods set>
or val>
.+Set
takes an argument function that is called whenever a field is assigned a value. To convert all user input to uppercase (gui '(+Set +TextField) uppc 30)
+Val
is an addition to +Set
. It accepts a function that is called whenever the field value is retrieved. To return the square of the field value (gui '(+Val +NumField) '((N) (* NN)) 10)
+Fmt
- it's just a combination +Set
and +Val
, it takes two functional arguments. In this example, the text will be displayed in capital letters, while returning in lowercase (gui '(+Fmt +TextField) uppc lowc 30)
+Map
(alike +Fmt
) produces two-way translation. It uses a list of cons pairs for linear searches, where CARs represent display values ​​that are internally mapped to values ​​in the CDR. If the value is not found in this list in the process set>
or val>
, it is passed 'as is'.Usually +Map
used in conjunction with a combobox (see Input fields ). This example displays the user as One, Two, and Three, but returns the number 1, 2, or 3 ######################################################################## (app) (action (html 0 "+Map" "@lib.css" NIL (form NIL (gui '(+Map +TextField) '(("One" . 1) ("Two" . 2) ("Three" . 3)) '("One" "Two" "Three") ) (gui '(+Button) "Print" '(msg (val> (field -1))) ) ) ) ) ########################################################################
Side effects
Whenever a button is clicked in a GUI, any changes made action
in the current environment (for example, the state of the database or application) should be reflected in the corresponding fields of the GUI. To do this, a message is sent to all components (on the server side) upd>
. Each component then takes appropriate measures (for example, updating from database objects, loading values ​​from variables, or calculating new values) to update its value.While the method upd>
is mainly used by the framework itself, it can be overridden in existing classes through the class prefix +Upd
. Let's display the updated values ​​in STDERR ######################################################################## (app) (default *Number 0) (action (html 0 "+Upd" "@lib.css" NIL (form NIL (gui '(+Upd +Var +NumField) '(prog (extra) (msg *Number)) '*Number 8 ) (gui '(+JS +Button) "Increment" '(inc '*Number) ) ) ) ) ########################################################################
Validation
To enable automatic verification of the entered data, a message is sent to all components at the appropriate time chk>
. If the value is valid, the corresponding method should return NIL
, otherwise the string describing the error.Many of the built-in classes have a method chk>
. The class +NumField
checks the correctness of the numeric entry, +DateField
- the correctness of the calendar date.On-the-fly testing can be performed using the prefix class +Chk
. The following code accepts only digits not exceeding 9: the expression or
first delegates the check to the main class +NumField
and — if it does not return an error — returns an error string when the current value is greater than 9. ######################################################################## (app) (action (html 0 "+Chk" "@lib.css" NIL (form NIL (gui '(+Chk +NumField) '(or (extra) (and (> (val> This) 9) "Number too big") ) 12 ) (gui '(+JS +Button) "Print" '(msg (val> (field -1))) ) ) ) ) ########################################################################
More straightforward checking is done by the built-in class +Limit
. It controls the attribute of the maxlength
created HTML input field. Thus, it is not possible to enter more characters than allowed in the field. ######################################################################## (app) (action (html 0 "+Limit" "@lib.css" NIL (form NIL (gui '(+Limit +TextField) 4 8) (gui '(+JS +Button) "Print" '(msg (val> (field -1))) ) ) ) ) ########################################################################
Data link
While set>
and val>
- formal methods to get the value of the GUI-component, they are not very often used explicitly. Instead, components are directly connected to Lisp's internal data structures, which are usually variables or database objects.The prefix class +Var
accepts a variable (in the function reference, it is described as a data type var
— a symbol or a cons pair). In the following example, we initialize the global variable with the value “abc” and allow the field +TextField
to work with it. The “Print” button can be used to display its current value. ######################################################################## (app) (setq *TextVariable "abc") (action (html 0 "+Var" "@lib.css" NIL (form NIL (gui '(+Var +TextField) '*TextVariable 8) (gui '(+JS +Button) "Print" '(msg *TextVariable) ) ) ) ) ########################################################################
+E/R
accepts an entity / relationship specification. This is a cons pair, with a relation in CAR (for example nm
, for an object name) and an expression in a CDR (usually (: home obj)
an object stored in the property of the obj
current form).In the following isolated and simple example, we create a temporary database and get access to the properties nr
and the nm
object stored in the global variable *Obj
. ######################################################################## (when (app) # On start of session (class +Tst +Entity) # Define data model (rel nr (+Number)) # with a number (rel nm (+String)) # and a string (pool (tmp "db")) # Create temporary DB (setq *Obj # and a single object (new! '(+Tst) 'nr 1 'nm "New Object") ) ) (action (html 0 "+E/R" "@lib.css" NIL (form NIL (gui '(+E/R +NumField) '(nr . *Obj) 8) # Linkage to 'nr' (gui '(+E/R +TextField) '(nm . *Obj) 20) # Linkage to 'nm' (gui '(+JS +Button) "Show" # Show the object '(out 2 (show *Obj)) ) ) ) ) # on standard error ########################################################################
Buttons
Buttons are, as described in Control Flow , the only way to interact with the application server (through POST requests).Basically +Button
takes- a name that can be a string or the name of an image file
- optional alternate name that can be displayed when the button is disabled
- and executable expression.
Below is an example of a minimal button, with a name and expression: (gui '(+Button) "Label" '(doSomething))
And this is a display of various names, depending on the state of the button: (gui '(+Button) "Enabled" "Disabled" '(doSomething))
To display an image instead of plain text, the label must be preceded by the symbol T
: (gui '(+Button) T "img/enabled.png" "img/disabled.png" '(doSomething))
The expression will be executed during processing action
(see Forms ) when this button is pressed.Like other components, the button can be expanded and combined with prefix classes; for this, a number of predefined classes and their combinations are available.Dialog Buttons
Buttons are important for handling alerts and dialog boxes . In addition to buttons for common functions, such as scrolling in tables or other side effects, there are special buttons that can close an alert or dialog box in addition to their main functions.Such buttons are typically subclasses +Close
, and most of them can be easily caused using prefabricated type functions closeButton
, cancelButton
, yesButton
or noButton
. We have seen several examples in alerts and dialogs .Active JavaScript
When a button is inherited from a class +JS
(and JavaScript is enabled in the browser), it is possible that the button will show a much faster response to clicks.The reason for this is that +JS
instead of the usual POST, it first tries to send only the contents of all GUI components to the server via XMLHttpRequest, and get the updated values ​​in response. This avoids the flicker caused by re-loading and rendering of the entire page, it is much faster, moreover it does not cause scrolling to the top of the page if it is larger than the browser window. This is especially noticeable when scrolling through tables.Only if this fails, the form will be sent with a regular POST request.So it makes no sense to use the prefix+JS
for buttons that trigger an HTML code change, open a dialog box or go to another page. In such cases, the overall performance will be even worse, because XMLHttpRequest will be called first.When JavaScript is disabled in the browser, XMLHttpRequest will not be used at all. The form will be fully usable, with exactly the same functionality, just a little slower and not so smooth.
Minimum finished application
The PicoLisp distribution includes a minimal but complete application in the app / directory. This application is typical, in the sense that it implements many of the methods described in this document, and it can be easily modified and extended. In practice, we use it as a template for developing our own applications.This is a kind of simplified ERP-system containing customers / suppliers, products (positions), orders and other data. The order entry form performs live updates of customer and product selection, pricing, inventory, and total cost calculation and creates PDF documents on the fly. Access is fine-tuned using users, roles, and permissions. The application is localized in six languages ​​(English, Spanish, German, Norwegian, Russian and Japanese), has some basic data and two sample reports.
Getting started
For a global installation (see Installation ), create a symbolic link to the place where the program files are installed. This is necessary because the application requires read / write access to the current working directory (for the database and other data at run time). $ ln -s /usr/share/picolisp/app
As always, you can run the application in debug mode. $ pil app/main.l -main -go +
or in production mode (non-debug) $ pil app/main.l -main -go -wait
and go to ' http: // localhost: 8080 ' in your browser. You can log in as “admin” user, password “admin”. The demonstration data contains several other users, but their roles are more limited in rights.Another possibility is to try the online version of this application on app.7fach.de .Localization
Before or after you are logged in, you can select another language and click on the “Change” button. This will affect all GUI components (but not the text from the database), as well as numeric formats, dates, and phone numbers.Navigation
The navigation menu on the left side shows two items “Home” and “logout” and three submenus “Data”, “Report” and “System”.Both “Home” and “logout” will switch you back to the initial login form. If you want to switch to another user (say, for a different role), use “logout” and — more importantly — log out before you close the browser to avoid possible blocking on the server.The “Data” submenu provides access to application data and maintenance: orders, products, customers and suppliers. The Report submenu contains two simple reports on inventory and sales. The System submenu allows you to administer user rights and their roles.Each submenu can be closed and opened independently. The presence of several simultaneously open submenus allows you to quickly switch between different parts of the application.The active menu item is indicated by a different list bullet style.Object selection
Each item in the “Data” or “System” submenu opens a search dialog box for this class of entities. You can specify a search pattern, click the Search button (or just ENTER) and scroll through the list of results.While in “Role” and “User” for entities, simple dialogs are provided (search by name only), other types of entities can be found by various criteria. In these cases, the "Reset" button clears the contents of the entire dialog box. A new object can be created with the lower right button “New”.In any case, the first column will contain either a “@” link (to go to this object) or an “@” button (to insert a link to this object in the current form).By default, the search returns all database objects with an attribute value greater than or equal tosearch criteria. Comparison is done arithmetically for numbers and alphabetically (case sensitive!) For text. This means that if you enter “Free” in the “City” field in the “Customer / Supplier” dialog, then the value of “Freetown” will match the search criteria. On the other hand, entering “free” or “town” will not find “Freetown.”Some search fields, however, show different behavior depending on the application:- The names of individuals, companies, or products allow tolerant searching, matching a slightly incorrectly written name (“Mühler” instead of “Miller”) or, for example, a substring (“Oaks” will correspond to “Seven Oaks Ltd.”).
- In the search field, an upper limit can be specified instead of a lower one, which will lead to a search for database objects with an attribute value less than or equal to the search criteria. This is useful, for example, in the Order dialog box to list the orders according to their number or date, starting with the most recent ones.
Using the scroll buttons, you can scroll through the list of results without restriction. Clicking on the link will display the corresponding object. Be careful to select the desired column: some dialogs (“Item” and “Order”) also provide links to related entities (for example, “supplier”).Editing
A database object is usually displayed in its own individual form, which is determined by its entity class.The basic layout should be consistent for all classes: under the title (which usually matches the menu item) is the object ID (name, number, etc.), and then the line with the Edit button on the left side and the Delete buttons, "Select" and two navigation links on the right side.The form is initially displayed as read only. This is necessary to prevent multiple users from simultaneously modifying an object (unlike previous PicoLisp Java frameworks, where this was not a problem, because all changes were immediately reflected in other users' GUIs).So if you want to change an object, you need to get exclusive access by clicking on the “Edit” button. The form is unlocked, and the Edit button changes to Done. If another user has already blocked this object, you will see a message with its name and process ID.The exceptions are objects that have just been created with "New". They will automatically be reserved for you, and the “Edit” button will be displayed as “Done”.The “Delete” button displays a dialog asking for confirmation. If the object is indeed deleted, this button changes to “Restore” and allows you to restore the object. Note that objects are never completely removed from the database as long as they are referenced from other objects. When a “deleted” object is displayed, its identifier is displayed in square brackets.The “Select” button (re) displays a search dialog for this entity class. Search criteria are saved between calls to search dialogs.Navigation links pointing left and right serve a similar purpose. They allow you to step by step through all the objects in a given class, in order of index.Other buttons, depending on the entity, are usually located at the bottom of the form. At the bottom right, there should always be another “Edit” / “Done” button.As we said in the chapter on Scrolling, any button in the form saves the changes to the underlying data model. However, as a special case, the “Done” button releases the object and returns to “Edit”. In addition, the editing mode will be interrupted as soon as another object appears, called by clicking on the object link (pencil icon), the top right navigation link, or the link in the search dialog box.Vs link buttons
The only way to interact with the HTTP-based application server is either to click on the HTML link, or on the submit button (see also control flow ). It is important to understand the various effects of such clicking on data entered or modified in the current form.- By clicking on the link, we leave or reload the page. Changes are undone.
- By clicking on the button, we apply the changes and perform the appropriate actions.
For this reason, the layout of the structure should clearly distinguish between links and buttons. Buttons in the form of images is not a good idea when in other places images are used for links. Standard buttons are preferred; they are usually displayed by the browser in an unambiguous three-dimensional form.Please note that if JavaScript is enabled in the browser, the changes will be automatically sent to the server.The on or off state of a button is an integral part of the application logic. This should be indicated to the user by appropriate styles.
Data model
The data model for this gadget consists of only six classes of entities (see Figure E / R at the beginning of “app / er.l”):### Entity / Relations ###
#
# nr nm nr nm nm
# | | | | |
# + - * ---- * - + + - * ---- * - + + - * ----- +
# | | sup | | | |
# str --* CuSu O-----------------* Item *-- inv | Role @-- perm
# | | | | | |
# +-*-*--O-+ +----O---+ +----@---+
# | | | | | usr
# nm tel -+ | | | |
# | | | | itm | role
# +-*-----+ | | +-------+ +---*---+ +----*---+
# | | | | | | ord | | | |
# | Sal +---+ +---* Ord @--------* Pos | nm --* User *-- pw
# | | cus | | pos | | | |
# +-*---*-+ +-*---*-+ +-*---*-+ +--------+
# | | | | | |
# hi sex nr dat pr cnt
- —
+CuSu
(/), +Item
() +Ord
(). +Pos
.+Role
+User
.
Classes +Role
and are +User
defined in "@ lib / adm.l". +Role
has a name, a list of permissions, and a list of users assigned to this role. +User
has a name, password and role.In app / er.l, the class +Role
expands to define a method for it url>
. Any object whose class has such a method is able to display itself in the GUI. In this case, the file “app / role.l” will be loaded — with a global variable *ID
pointing to it — each time the HTML link to this object is activated.The class is +User
also expanded. In addition to login, the full name, e-mail and phone number are added. And, of course, the omnipresent method url>
.Application logic revolves around orders. Order has number, date, customer (copy+CuSu
) and a list of positions (objects +Pos
). The method sum>
calculates the total cost of this order.Each item has a link to the object +Item
(product), price and quantity. The price in the position overrides the default price for the corresponding item.Each product has a number, description, supplier (also a copy +CuSu
), inventory accounting (number of items from the last inventory) and price. The method cnt>
calculates the current stock of this item as the difference between the inventory and the quantity of the item sold.Function calldbs
at the end of “app / er.l” configures the physical structure of the database. Each of the provided lists has a number in CAR, which determines the block size of the corresponding database file as a power of 64 (64 << N). The CDR determines that instances of this class (if the element is a class symbol) or tree nodes (if the element is a list of the class symbol and property name) must be placed in this file. This allows for some optimization in the database structure. # Database sizes (dbs (3 +Role +User +Sal) # 512 Prevalent objects (0 +Pos) # A:64 Tiny objects (1 +Item +Ord) # B:128 Small objects (2 +CuSu) # C:256 Normal objects (2 (+Role nm) (+User nm) (+Sal nm)) # D:256 Small indexes (4 (+CuSu nr plz tel mob)) # E:1024 Normal indexes (4 (+CuSu nm)) # F:1024 (4 (+CuSu ort)) # G:1024 (4 (+Item nr sup pr)) # H:1024 (4 (+Item nm)) # I:1024 (4 (+Ord nr dat cus)) # J:1024 (4 (+Pos itm)) ) # K:1024
Using
After connecting to the application (see Getting Started ), you can try to do some “real” work with it. Through the “Data” menu (see navigation ), you can create or edit customers, suppliers, products and orders, and create simple reports through the “Report” menu.Customer / Supplier
“App / cusu.l” Theclient / provider search dialog ( choCuSu
in “app / gui.l”) supports many search criteria. They become necessary when the database contains a large number of clients, and can be filtered by zip, phone number prefixes, and so on.In addition to the main layout (see Editing ), the form is divided into four separate tabs. Splitting the form into several tabs allows you to reduce traffic, with a possible faster GUI response. In this case, perhaps four tabs are unnecessary, but quite suitable for demonstration purposes, and they leave room for extensions.Keep in mind that when the data has been changed in one of the tabs, the “Done” button must be pressed before selecting another tab, because the tabs are implemented as HTML links (see Buttons vs links ).For new customers or suppliers, the next available number will be automatically assigned. You can enter another number, but if you try to use an existing number, an error will occur. The field "Name" is mandatory.Phone and fax numbers in the “Contact” tab should be entered in the correct format, depending on the locale (see Phone numbers ).The “Memo” tab contains one text area. You can use it for large pieces of text, as it is stored in a blob object.Product
“App / item.l”Products also have a unique number and a required field “Description”.To assign a supplier, click on the "+" button. The customer / supplier search dialog box will appear, and you can select the desired supplier by clicking the "@" button in the first column. In addition, if you are sure that you know the exact spelling of the supplier’s name, you can also type it directly in the text field.In the Search dialog box, you can also click on the link, for example, to explore possible suppliers and then return to the search window by clicking the back button in the browser. The “Edit” mode will of course be canceled, since you have moved to another object (this is described in the last part of the editing ).You can enter inventory - the quantity of goods currently in stock. The next field automatically reflects the quantity of the remaining product after a certain quantity has been sold (that is, they have links in the order items). This value cannot be changed manually.Pricing must be entered with a decimal separator depending on the current locale. It will be formatted with two characters after the decimal separator.The “Memo” field, as well as in the Client / provider above, is stored in the DB blob object.Finally, a JPEG image can be stored in a blob for this item. Select a file using the file selection dialog and click on the “Install” button. The image will appear at the bottom of the page, and the Set button changes to Delete, allowing you to delete the image.Order
“App / ord.l”Orders are identified by number and date.The number must be unique. It is assigned when an order is created and cannot be changed.The date for the newly created order is set to today, but can be changed manually. The date format depends on the locale. These are YYYY-MM-DD (ISO) by default, DD.MM.YYYY in German and YYYY / MM / DD in Japanese. As described in the time and date , this field allows abbreviated input, for example, just enter the day to get the full date in the current month.To assign a customer to this order, click on the "+" button. The Search Client / Supplier dialog box appears, and you can select the desired client with the “@” button in the first column (or enter the name directly in the text field), as described above for the product .Now enter the order items: select the product using the "+" button. In the field "Price" will be the default price, you can change it manually. Then enter the quantity and press the button (usually the “+” button to select the next item or the scroll button). The form will be automatically recalculated to show the total value of the items and the entire order.Instead of “+” or the scroll button, as recommended above, of course, you can also click the “Done” button to save the changes. But this will cause the button to be pressed a second time (now “Edit”) if you want to continue to fill positions.The “x” button on the right side of each position deletes this position without further confirmation. It must be used with care!The “^” button swaps the current line with the parent line. Thus, it can be used to change the location of all the elements in the table, by raising them to the desired position.The “PDF-Print” button creates and displays a PDF document for this order. The browser must be configured to display downloaded PDF documents in the appropriate viewer. The source of the postscript-creating method is in “app / lib.l”. It produces one or more A4 pages, depending on the number of positions.Reports
“App / inventory.l and“ app / sales.l ”Two reports (“ Inventory ”and“ Sales ”) contain several search fields and a“ Show ”button.If the search criteria are not specified, the “Show” button will display a list of the relevant part of the entire database. This can take a long time and cause a heavy load on the browser if the database is large.So in the normal case, you will limit the selection by specifying the number range, description template and / or supplier for the inventory report or range of order dates and / or customer for the sales report. If the corresponding value is omitted, objects are not filtered by this criterion.At the end of each report, a “CSV” link appears. It loads the file generated in the report.