FS FormScript

State

A form sometimes needs to remember something that is not an answer: which page the person was on, how far they had got, what they had typed so far.

FormScript has a block for that. A state block declares a named snapshot of runtime state:

forms
form applicant version = "1.0" {
  page "applicant" {
    label = "Applicant"

    section identity {
      field first_name { type = string }
      field last_name  { type = string }
    }
  }

  state draft {
    currentPage = "applicant"
    filled      = filledFields()
  }
}

The members of draft are expressions, evaluated in the current transaction. The runtime exposes the resulting map under the name draft, so draft.currentPage reads where the person was.

Why it exists

It is the supported way to write save-and-resume without a runtime-specific API. A runtime that renders one page at a time needs to know which page; a runtime that lets a person leave and come back needs to know what they had typed. Both are runtime state, and state is how a document asks for it.

The four builtins that read the form rather than a value are what you build it from:

The call What it gives you
filledFields() the paths of the fields that hold an answer
isActive(path) is that field being asked?
isOverridden(path) was that field's answer replaced?
shadow(path) the value a field held before it stopped applying
forms-fragment
state progress {
  filled    = filledFields()
  guarantor = isActive(["guarantor", "name"])
  retained  = shadow(["address", "postcode"])
}

A state is not a field

Three things follow from that:

  • a state block does not declare a data model — it holds expressions, not answers;
  • a state block must not appear in the output document;
  • the members of a state can read the form's fields and the builtins, and nothing else.

Where a state name lives

This is the rule that was clarified in the specification, because the old wording was ambiguous.

A state name is a declaration in the form root scope, wherever the state block is written, and is visible to every expression in the form. (Spec-8.7)

So:

forms
form applicant version = "1.0" {
  group personal {
    field first_name { type = string }

    // This is still a root-scope declaration, even though it is written here.
    state draft {
      page = "identity"
    }
  }

  field greeting { type = string value = draft.page }
}

draft is visible everywhere in the form, because it belongs to the form, not to the group it happens to be written in.

And the members of a state are resolved in the root scope as well:

forms-invalid
// E-REF-01: "first_name" is not declared in the root scope.
form applicant version = "1.0" {
  group personal {
    field first_name { type = string }
  }

  state draft {
    who = first_name
  }
}

Inside draft, first_name is looked for in the form root — and the form root has no field called first_name, because the field belongs to the group personal. To read it, write the path: personal.first_name.

Names must not collide

A state name must not shadow a field name or any other declaration of the form root scope. Two state blocks with the same name are two declarations in the same scope, and the second one is an error — E-REF-02.

forms-invalid
// E-REF-02: the state collides with the field.
form applicant version = "1.0" {
  field draft { type = string }   // E-REF-02: a state named "draft" collides

  state draft {
    page = "identity"
  }
}

The same code is an error in the reference implementation and in the second runtime, and both say E-REF-02.

A state and a transaction

A state is evaluated in the current transaction. It is not a stored document and it is not written by the document — it is a view the runtime exposes while the form is being answered. When the person saves, the runtime saves the answers, and the state is rebuilt from them.