A stateful web app in a pico

In an earlier post, we showed a simple web application in a pico. Each time that application is requested, it produces a different page, but it doesn't maintain state from one request to the next.

Here we will discuss a simple application that allows a visitor to select their favorite color. Once the selection has been made, each time the page will show that color, and allow them to change it.

This demonstrates the way the Pico Stack persists state without needing an external database.

Screenshots

Just before selecting orange:


After, showing that the selection is remembered:


The code

The code for this application is in three rulesets (two of which are used as modules by the third):

  • html.krl (41 lines)
  • css2colors.krl (37 lines)
  • fav-color-sample.krl (86 lines)

These rulesets can be found in this folder in the PicoStack repo.

Overview

We'll show first the function index that produces the HTML string that codes for the web app page, and then the rule recordFavColor which reacts to the visitor's color selection.

Producing the web page

ruleset fav-color-sample {
  meta {
    use module html
    shares index
  }
  global {
    index = function(){
      url = <<#{meta:host}/sky/event/#{meta:eci}/sel/fav_color/fav_color_selected>>
      html:header("Favorite Color","")
      + <<
<h1>Favorite Color</h1>
#{ent:colorname => <<<p>Your favorite color:</p>
...
>> | <<<p>You have not yet selected a favorite color.</p>
>>}<hr>
<form action="#{url}" method="POST">
...
</form>
>>
      + html:footer()
    }
  }
}

Note the ruleset name and the function name, which both appear in the URL that we use to request the page. In this URL the parts in all caps will be replaced with specific values (for the domain and port of the pico engine which hosts this pico, and the event channel identifier of the pico):

https://DOMAIN:PORT/sky/cloud/ECI/fav-color-sample/index.html

Of interest is the mention of a persistent entity variable, ent:colorname, which, if present means that the color selection has been made, and we'll produce the HTML paragraph and other tags in the first ellipsis. If it has not been defined, then we'll present the message following the vertical bar (another example of a ternary Boolean if-then-else, inside of a beesting). In either case there will be a horizontal rule and the form sketched in around the second ellipsis.

Notice the value bound to the name url first thing in the function. This is used as the action of the form, and segues into the next section. This is an event URL that, when the form is submitted, will send a message to the pico (and trigger the rule discussed in the next section):

https://DOMAIN:PORT/sky/event/ECI/sel/fav_color/fav_color_selected

Storing the selected color

ruleset fav-color-sample {
  meta {
    use module css2colors alias colors
  }
  rule recordFavColor {
    select when fav_color fav_color_selected
      fav_color re#^(\#[a-f0-9]{6})$# setting(fav_color)
    pre {
      colorname = colors:colormap()
        .filter(function(v){v==fav_color}).keys().head()
      || "unknown"
    }
    fired {
      ent:colorname := colorname
      ent:colorcode := fav_color
    }
  }
}

This rule selects on the event described above. Notice how the two identifiers in the select when clause exactly match the last two path components of the URL.

The next line of the rule names an expected event attribute (from the form), fav_color, which must match the regular expression following it (a leading "#" sign, followed by six hexadecimal digits), or the rule will not be selected. If the attribute is present, and matches the pattern, the captured portion will be bound to the name fav_color by the setting sub-clause.

When the rule is selected, it will evaluate, looking up the color name from the Map provided by the css2colors module's colormap function (or "unknown" if the code is not found in the Map).

Since the rule has fired, the name and code will be assigned to the two entity variables. These are persistent variables, and that is how the ruleset maintains its state in the pico.

Multiple users

You can install these rulesets in your pico and I can install them in mine. We will each have our own instance of the favorite color application in our pico. 

They will have distinct color choices. The URLs we use will be different in the ECI path component, but the same otherwise. They may differ in the domain and port components if our picos are not on the same pico engine.

Comparison to other languages

If you were to think of this ruleset from a Java mindset, it would be a class with two instance variables, with getters and setters, and you'd have to connect it to a separate application server, and implement the web application using a couple of JSP files. Once all that was in place, you could instantiate the class a couple of times: once for you and once for me. Since we want it to be persistent (not just in memory while the JVM is running), we'd have to set up and connect to a database, etc. We might use an ORM to connect a database record to each instance of the class.

With picos, our picos already exist (as do thousands at the time of this writing, throughout the world). Any pre-existing pico could install these three rulesets. Once installed, these picos would each have their own color selection capability, independent of all the others.

Technical details

This section will show some annotated code snippets, with some explanation of what is going on.

Table displaying selected color

In the case where there is a saved favorite color, it is displayed in tabular form. We refer the reader to the actual code, which is almost entirely HTML. There are just three beestings, bringing in the color name and code from entity variables.

Generating the color options

This happens in a select tag inside the form, which is always displayed, whether a color has been selected or not.

The selection form

The HTML code for the form is shown here:

<form action="#{url}" method="POST">
Favorite color: <select name="fav_color">
#{colors:options("  ",ent:colorname)}</select>
<button type="submit">Select</button>
</form>

Notice that the colors module is asked to generate the options, with each indented by two spaces, and with the selected color as the default (i.e. selected option).

The colors module

The css2colors ruleset provides a function named options() taking two arguments: spaces to indent each option tag, and the default color name.

    options = function(indent,default){
      left_margin = indent || ""
      gen_option = function(v,k){
        <<#{left_margin}<option value="#{v}"#{
            k==default => " selected" | ""
          }>#{k}</option>
>>
      }
      colors.map(gen_option).values().join("")
    }

Either function argument can be omitted. The left margin will default to none (the empty string), and a missing default color name will be null and will not match.

The internal function gen_option will produce a one line string with some left margin spaces, the opening option tag with the value taken from its first argument, possibly inserting the string " selected" to mark this option as the one to position the list at, its second argument (a color name) to appear in the dropdown list, and finally the closing option tag. Notice that each time the string ends with a newline.

The return value, a string containing all the options, is generated by mapping the gen_option function over all of the entries in the colors map, and taking the computed values (each a complete option element) and joining them together into a single string (which replaces the beesting that calls the function).

Redirection back to the same page

A final rule in the application ruleset, which also selects on the event, does a redirection back to the referrer page.

Putting it all together

To see this web application in action, you'll need to do these things: 

  1. have control of a server (either on the Internet, or just your local machine)
  2. install the pico engine on that machine (see instructions)
  3. start the developer UI
  4. create a pico (or just use the Root pico)
  5. install the rulesets listed above (in order) in your pico (be sure to use raw URLs)
  6. find the channel that allows access to the third ruleset
  7. from your browser visit the resource described by a URL following the first (/sky/cloud) pattern above and tailored to your pico engine's domain and port and the identifier of the channel you found in step 6

No comments:

Post a Comment