senro › guides

senro guides

views and components

senro's view layer is kata. a view is a .kata file, compiled to a kanso function while the app compiles, so the compiler checks a template the way it checks any other code. this guide covers the format, layouts, components, data from the server, and pages rendered in the browser.

templates

templates live in a directory of their own that holds no kanso source, the guestbook's views. the entry imports the directory and writes derive kata/views, and each file becomes a public function of that module named after it: views/signed.kata is called as views/signed.

examples/guestbook/views/signed.kata
takes signatures form flash removals viewer
header
  h1 Guestbook
  p.lede Leave a line for whoever comes next.
if viewer == ""
  nav.door
    a href="/session/new" Sign in
    a href="/account/new" Make an account
else
  form.out method="post" action="/session"
    input type="hidden" name="_method" value="DELETE"
    span Signed in as {viewer}.
    a href="/account/edit" Your account
    button Sign out
if flash != ""
  p.flash = flash
if viewer != ""
  form.sign method="post" action="/entries" enctype="multipart/form-data"
    textarea name="message" placeholder="say something" required="required" = form.message
    if form.problems["message"] != none
      p.problem = form.problems["message"]
    label
      span A photo, if you like
      input type="file" name="photo" accept="image/*"
    button Sign
ol.entries
  for e in signatures
    li
      p.message = e.message
      if e.photo != ""
        img.photo src="/senro/blobs/{e.photo}" alt="a photo from {e.name}"
      p.who = e.name
      == removals["{e.id}"]

the first line, takes, names the parameters in order. nesting is by indentation and tags close themselves. tag.class#id is shorthand, and a bare .class means a div. an attribute is name="text", where the text may hold {expr}, or name=expr for a value. = expr writes a value escaped and == expr writes it raw. the control lines are if, else if, else, for x in xs, and case expr with when value rows under it and an optional else, and # starts a comment. a when matches its value as a function's argument is matched, so it names a string, a number, true, false or none, and a value no when names writes the else rows or nothing.

because the template is compiled to kanso, kanso check reads it. a hole may read through a record a value holds, as {p.author.name} does for a post whose author is a record of its own. a field the template reads that no record in the app has is a compile error. the message quotes the line of the source kata wrote and adds the template line it came from: a misspelled e.mesage on line 29 of signed.kata adds = written from ./views/signed.kata:29. kanso derived prints the whole source kata wrote.

layouts

a layout is a template that takes the page's body and writes it raw. the guestbook's loads senro's page script, which is what lets a page update in place.

examples/guestbook/views/layout.kata
takes title body
doctype html
html
  head
    meta charset="utf-8"
    meta name="viewport" content="width=device-width, initial-scale=1"
    title = title
    link rel="stylesheet" href="/site.css"
    script src="/senro.js" defer="defer"
  body
    main
      == body

components

a template that binds an action is a component. on-click=more binds a click on an element to the function more in the template's module. a component takes one value, its state, and an action takes the state and answers the next one. the pantry's meal planner counts the week's meals, and its state is a map holding the count:

examples/pantry/planner/view.kata
takes plan
div.planner
  span.label Meals planned this week
  button.minus on-click=fewer aria-label="one fewer" −
  span.count = plan["meals"]
  button.plus on-click=more aria-label="one more" +
  button.check on-click=checked Check the staples
  case plan["note"]
    when none
    else
      p.note = plan["note"]
examples/pantry/planner/planner.kso
# The week's meal plan, counted in the browser, and the staples it would run
# short of. These are the component's actions: each takes the plan and
# answers the next one, and the planner's template binds them to its buttons.
# `more` and `fewer` count in the browser alone and clear the last check;
# `checked` asks the server.
pub fn more plan
  planned (meals_of plan + 1)

pub fn fewer plan
  n = meals_of plan
  planned (if (n > 0) (n - 1) 0)

where an action runs depends on what it asks for. an action that takes only the state runs in the browser, in the app's browser module, without asking the server. one that also asks for req, db or viewer runs on the server: the click posts the state to /senro/act/<component>, and the page morphs the component's next rendering into place. the guestbook's remove button is that kind:

examples/guestbook/removal/view.kata
takes id
button.remove on-click=removed aria-label="remove entry {id}" ×
examples/guestbook/removal/removal.kso
import "../../../models"
import "../signatures"

# Removes an entry. The action asks for the viewer and a database session, so
# it runs on the server. Its state is the entry's id, which anyone can send,
# so it removes the entry through the model, whose policy says no to anyone
# but the person who signed it, and a no is the answer.
pub fn removed viewer db id
  models/destroyed viewer db signatures/signature_model id
    .> (w -> settled id w)

fn settled id w
  if (models/refused? w) w id

its state is the entry's id, which anyone can send, so the action removes the entry through the model, whose policy says no to anyone but the signer. kata refuses a server action that calls pg/query or pg/run itself: every write a component makes goes through a policy.

data from the server

an action that runs in the browser can still read what only the server holds, through a provider. a provider is a function whose first two parameters are the viewer and a database session. the planner's check button asks which staples the week would run short of, and the answer comes from the shelf in the database:

examples/pantry/stock.kso
# The staples the shelf holds fewer of than `meals`, by name. The planner asks
# for them as `stock/short meals`, and this body runs only on the server.
pub fn short viewer db meals
  models/where viewer db item_model "staple" "t"
    .> (items -> effect (under items meals))

fn under items meals
  low = list/select items (i -> i.quantity < meals)
  list/to_list (list/map low (i -> i.name))

the action calls the provider with two arguments fewer, as it calls any function. with derive data/providers in the entry, the derivation writes that shorter short meals into stock.kso, and called in the browser it asks the server for the answer:

examples/pantry/planner/planner.kso
pub fn checked plan
  asked = stock/short (meals_of plan)
  data/then asked (names -> put plan "note" (noted names))

checked answers what it asks for instead of its next state. the page sends the call to POST /senro/data, runs the action again with the answer, and renders the plan it comes to: one click, one request. calls an action gathers with data/all go in one request however many there are, and a call inside data/then that needs an earlier answer waits a round. the server answers from the table the derivation writes beside the providers, stock/provided:

examples/pantry/main.kso
# The planner asks the server for the staples the week runs short of, through
# the provider in stock.kso.
answering = web/components routes secret served
served_with_data = web/providers answering secret stock/provided

a page can reach only a provider the app's own code calls that way, and only from senro's page script. anyone with a session can still call one with arguments of their choosing, so a provider reads through a model, whose policy decides what the viewer may see. routes/rest gives a provider no route, so it can sit in a resource's module beside the actions. the browser module compiles the provider's body without running it, so the pantry's browser entry derives the models too.

pages in the browser

when an app answers its pages with views/answer or views/answer_as, kata writes two more functions per page. signed_view describes the page as data, the template's name and the values it reads, and signed_keyed renders the template from that data. the first request gets the page rendered on the server as HTML. once the browser module has loaded, a link followed or a form sent on the app's own origin fetches the page as data and renders it in the browser with the same template compiled to wasm.

examples/guestbook/browser.kso
import "../../browser"
import "../../kata"
import "./views"

derive kata/views

# The guestbook's pages, compiled for the browser with
# `kanso build browser.kso --wasm`. Its one component, the remove button,
# runs on the server, so the module holds no component of its own.
browser/answer_pages {} views/rendered

the module is built with kanso build browser.kso --wasm. senro's page script keeps it in the browser's Cache Storage under its modification time, so a returning visitor does not download it again, and a page opened before a deploy loads itself again once. kanso's differential law is what makes this safe: every engine answers the same bytes, so the browser's rendering of a page and the server's cannot drift apart. senro's specs render a page both ways and compare them byte for byte.

live pages

when a request that is not a GET finishes, the server notifies Postgres's senro channel, and every page holding /senro/live open fetches itself again. one visitor's signature appears on another's screen without a reload, and a field someone is typing in keeps what they typed, unless it was in the form they just sent.