FS FormScript

Conditional questions

Some questions only make sense in some circumstances. A guarantor is asked when the loan is large. A second address is asked when the country is not the United Kingdom.

The when property says so:

forms
form loan version = "1.0" {
  field loan_amount    { type = number }
  field guarantor_name { type = string  when = loan_amount > 10000 }
}

What when means

A field whose when is false is inapplicable. That is a stronger word than "hidden":

  • it is not rendered;
  • it is absent from the output document — not null, absent;
  • it does not block submission;
  • its constraints do not run.

A question that is not asked cannot be answered wrongly.

The comparison is three-valued

loan_amount > 10000 is not always true or false. Before the applicant has typed the amount, it is unknown — and an unknown condition makes the field inapplicable.

So the field is not asked at the start of the form, and it appears once the amount is high enough. That is the behaviour you want: a form that demanded a guarantor from everybody would be a form nobody finishes.

The rule in one line: when is true → applicable; when is false or unknown → inapplicable.

A guard that reads another group

A guard can read a field in a group, by path:

forms
form loan version = "1.0" {
  group applicant {
    field income { type = number }
  }

  field guarantor_name { type = string when = applicant.income > 10000 }
}

applicant.income is a dotted path that names a field, so the value of the path is that field's own answer — the one the applicant typed, not the group's composition as it stood at some earlier moment.

That precision is not academic. A guard that reads a stale value asks the wrong question, and the specification was amended to say exactly which value a dotted path names (Spec-6.20) after a real disagreement between two runtimes about it.

A guard on a group or a repeat

when works on anything, not only on fields:

forms
form loan version = "1.0" {
  group applicant {
    field income { type = number }
  }

  group guarantor {
    when = applicant.income > 10000
    field name { type = string }
  }
}

When the group is inapplicable, everything inside it is too.

A repeat whose guard changes

A repeat grows and shrinks as the person adds and removes rows, and each row can have its own guard:

forms
form household version = "1.0" {
  repeat dependants {
    max = 5
    field name { type = string }
    field age  { type = integer }

    field school { type = string when = age < 18 }
  }
}

The school question appears in a row once that row's age says the dependant is a child — and it disappears again if the age is corrected.

When the guard is read

This is the part that is easy to get wrong, so the specification states it plainly (Spec-9.12):

A structural change to a repeat recomputes the applicability of every entity it holds, and a new element's guard is read in that element.

In practice:

  • when a repeat gains a row, the guards of every row are read again, not only the new row's;
  • a new row's guard is read in that row — with that row's own answers, not with the answers of the first row;
  • when an answer changes, the guards that read it are recomputed.

So a row whose school question was hidden because age was 30 does not stay hidden because a different row changed.

The shadow store

When a field stops applying, its answer is not thrown away — it is retained and can be read back with shadow(path):

forms-fragment
field previous_city { type = string value = shadow(["address", "city"]) }

That is what makes a form that hides and shows questions feel like a form rather than a series of separate forms.

isActive

To ask whether a field is being asked at all, there is a builtin:

forms-fragment
field summary { type = string value = isActive(["applicant", "guarantor_name"]) ? "Guarantor asked" : "No guarantor" }

isActive(path) is true when the field at that path is currently applicable. The path is written as a list of its segments — ["applicant", "guarantor_name"] — because a path is not a name, it is a place.

It is one of the four builtins that read the form rather than a value — see the builtin library.