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:
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 |
state progress {
filled = filledFields()
guarantor = isActive(["guarantor", "name"])
retained = shadow(["address", "postcode"])
}
A state is not a field
Three things follow from that:
- a
stateblock does not declare a data model — it holds expressions, not answers; - a
stateblock must not appear in the output document; - the members of a
statecan 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
statename is a declaration in the form root scope, wherever thestateblock is written, and is visible to every expression in the form. (Spec-8.7)
So:
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:
// 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.
// 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.