FS FormScript

Examples

Seven complete documents, each one running here in the browser, in the playground, and in the conformance suite. Filter the list, or jump straight to one.

Comprehensions over a collection

map, filter, sum, any, all and reduce over a repeat, with the bound identifier scoped to the body.

ExpressionsRepeats

Edit in playground

forms
form portfolio version = "1.0" {
  text {
    "p.ticker": "Ticker"
    "p.quantity": "Quantity"
    "p.price": "Price"
    "p.value": "Value"
    "p.total": "Total value"
    "p.largest": "Largest single holding"
    "p.howmany": "How many holdings"
    "p.overweight": "Any holding worth more than 10000"
    "p.positive": "All quantities positive"
    "p.tickers": "Tickers"
  }

  repeat holdings {
    max = 20
    field ticker { type = string, label = t("p.ticker") }
    field quantity { type = integer, label = t("p.quantity") }
    field price { type = number, label = t("p.price") }
    field value { type = number, label = t("p.value"), value = quantity * price }
  }

  field total {
    type = number
    label = t("p.total")
    value = sum(item in holdings, item.value)
  }

  field largest {
    type = number
    label = t("p.largest")
    value = maxOf(holdings)
  }

  field holding_count {
    type = integer
    label = t("p.howmany")
    value = count(holdings)
  }

  field any_overweight {
    type = boolean
    label = t("p.overweight")
    value = any(item in holdings, item.value > 10000)
  }

  field all_positive {
    type = boolean
    label = t("p.positive")
    value = all(item in holdings, item.quantity > 0)
  }

  field tickers {
    type = array of string
    label = t("p.tickers")
    value = map(item in holdings, item.ticker)
  }

  output {
    total: number
    largest: number
    holding_count: integer
    any_overweight: boolean
    all_positive: boolean
    tickers: array of string
  }
}

Live form

Conditionals and applicability

A `when` guard makes an entity applicable; an inapplicable entity is absent from the output document, not null.

LogicApplicability

Edit in playground

forms
form checkout version = "1.0" {
  text {
    "c.delivery": "Delivery"
    "c.address": "Delivery address"
    "c.postcode": "Postcode"
    "c.point": "Collection point"
    "c.note": "A note for the courier"
    "c.gift": "Gift message"
  }

  field delivery {
    type = enum("standard" = "Standard", "express" = "Express", "collect" = "Collect in store")
    label = t("c.delivery")
    default = "standard"
  }

  field address_line {
    type = string
    label = t("c.address")
    when = delivery != "collect"
  }

  field postcode {
    type = string
    label = t("c.postcode")
    when = delivery != "collect"
    validate { pattern = "^[A-Z]{2}[0-9] [0-9][A-Z]{2}$" }
  }

  field collection_point {
    type = string
    label = t("c.point")
    when = delivery == "collect"
  }

  field express_note {
    type = string
    label = t("c.note")
    when = delivery == "express"
  }

  field gift_message {
    type = string
    label = t("c.gift")
    when = delivery != "collect" and delivery != "express"
  }

  output {
    delivery: enum("standard", "express", "collect")
    address_line: string
    postcode: string
    collection_point: string
    express_note: string
    gift_message: string
  }
}

Live form

Every constraint kind

required, accepted, min, max, minLength, maxLength, pattern, email, phone, url, unique, in, notIn, multipleOf, equal, notEqual, cross and custom — one field each.

Validation

Edit in playground

forms
form constraints version = "1.0" {
  text {
    "f.age": "Age"
    "f.name": "Full name"
    "f.reference": "Reference number"
    "f.email": "Email"
    "f.phone": "Mobile number"
    "f.url": "Website"
    "f.code": "Country code"
    "f.quantity": "Quantity"
    "f.confirm": "Confirmation code"
    "f.country": "Country"
    "f.agree": "I agree"
    "errors.age": "Age must be between 0 and 130"
    "errors.agree": "You must agree to the terms"
  }

  field age {
    type = integer
    label = t("f.age")
    validate {
      required
      min = 0
      max = 130
    }
  }

  field name {
    type = string
    label = t("f.name")
    validate {
      required
      minLength = 2
      maxLength = 80
    }
  }

  field reference {
    type = string
    label = t("f.reference")
    validate { pattern = "^[A-Z]{2}-[0-9]{4}$" }
  }

  field email {
    type = string
    label = t("f.email")
    validate { email }
  }

  field phone {
    type = string
    label = t("f.phone")
    validate { phone }
  }

  field url {
    type = string
    label = t("f.url")
    validate { url }
  }

  field code {
    type = string
    label = t("f.code")
    validate { in = ["GB", "FR", "DE", "AR"] }
  }

  field country {
    type = string
    label = t("f.country")
    validate { notIn = ["XX", "YY"] }
  }

  field quantity {
    type = integer
    label = t("f.quantity")
    validate { multipleOf = 5 }
  }

  field confirm {
    type = string
    label = t("f.confirm")
    validate { equal = reference }
  }

  field other {
    type = string
    label = t("f.country")
    validate { notEqual = reference }
  }

  field agree {
    type = boolean
    label = t("f.agree")
    validate {
      accepted
      custom = agree ? true : t("errors.agree")
    }
  }

  output {
    age: integer
    name: string
    reference: string
    email: string
    phone: string
    url: string
    code: string
    country: string
    quantity: integer
    confirm: string
    other: string
    agree: boolean
  }
}

Live form

Repeats, groups and choices

A group contributes an object to the output document, a repeat is bounded by `max`, and a section adds no data level at all.

RepeatsGroups

Edit in playground

forms
form household version = "1.0" {
  text {
    "h.address": "Address"
    "h.street": "Street"
    "h.city": "City"
    "h.dependants": "Dependants"
    "h.name": "Name"
    "h.age": "Age"
    "h.student": "Student"
    "h.employment": "Employment status"
    "h.housing": "Housing"
  }

  section members {
    label = t("h.address")

    group address {
      field street { type = string, label = t("h.street") }
      field city   { type = string, label = t("h.city") }
    }

    repeat dependants {
      max = 5
      label = t("h.dependants")

      field name { type = string, label = t("h.name") }
      field age  { type = integer, label = t("h.age") }
      field is_student { type = boolean, label = t("h.student") }
    }
  }

  choice employment {
    label = t("h.employment")
    inline = true
    type = enum(
      "employed" = "Employed",
      "self" = "Self-employed",
      "unemployed" = "Unemployed",
      "retired" = "Retired"
    )
  }

  choice housing {
    label = t("h.housing")
    type = enum("own" = "Own", "rent" = "Rent", "other" = "Other")
  }

  output {
    address.street: string
    address.city: string
    dependants: array of object { name: string, age: integer, is_student: boolean }
    employment: enum("employed", "self", "unemployed", "retired")
    housing: enum("own", "rent", "other")
  }
}

Live form

Internationalization

Text lives in a text block, never inline; a locale entry overrides the default, and `t` resolves it.

LocaleText

Edit in playground

forms
form international version = "1.0" {
  text {
    "form.title": "Registration"
    "field.name": { "default": "Full name", "ar": "الاسم الكامل", "fr": "Nom complet" }
    "field.country": { "default": "Country", "fr": "Pays" }
    "form.submit": { "default": "Create the account", "ar": "إنشاء الحساب" }
    "errors.required": { "default": "This field is required", "fr": "Ce champ est obligatoire" }
  }

  field name {
    type = string
    label = t("field.name")
    validate { required }
  }

  field country {
    type = string
    label = t("field.country")
    default = "United Kingdom"
  }

  field accept_terms {
    type = boolean
    label = t("form.submit")
    validate {
      accepted
      custom = accept_terms ? true : t("errors.required")
    }
  }

  output {
    name: string
    country: string
    accept_terms: boolean
  }
}

Live form

A loan application

A group contributes an object to the output document, a payment is computed from a rate, and a guarantor is asked for only when it is needed.

GroupsCalculated values

Edit in playground

forms
form loan_application version = "1.0" {
  text {
    "applicant.first": "First name"
    "applicant.last": "Last name"
    "applicant.income": "Annual income"
    "applicant.amount": "Loan amount"
    "applicant.term": "Term in months"
    "applicant.monthly": "Monthly payment"
    "applicant.guarantor": "Guarantor name"
    "applicant.consent": "The applicant agrees to a credit check"
  }

  group applicant {
    field first_name {
      type = string
      label = t("applicant.first")
      required = true
    }
    field last_name {
      type = string
      label = t("applicant.last")
      required = true
    }
    field annual_income {
      type = number
      label = t("applicant.income")
      validate { min = 0 }
    }
    field loan_amount {
      type = number
      label = t("applicant.amount")
      validate { min = 0 }
    }
    field term {
      type = integer
      label = t("applicant.term")
      default = 60
    }
    field monthly {
      type = number
      label = t("applicant.monthly")
      value = pmt(0.05 / 12, term, loan_amount)
    }
  }

  group guarantor {
    field guarantor_name {
      type = string
      label = t("applicant.guarantor")
      when = applicant.annual_income > 10000
    }
    field guarantor_consent {
      type = boolean
      label = t("applicant.consent")
      when = applicant.annual_income > 10000
      validate { accepted }
    }
  }

  output {
    applicant.first_name: string
    applicant.last_name: string
    applicant.annual_income: number
    applicant.loan_amount: number
    applicant.term: integer
    applicant.monthly: number
    guarantor.guarantor_name: string
    guarantor.guarantor_consent: boolean
  }
}

Live form

Exact decimal arithmetic

Numbers are decimal128, not binary floating point; the canonical decimal form is what reaches the wire.

ArithmeticMoney

Edit in playground

forms
form invoice version = "1.0" {
  text {
    "i.unit": "Unit price"
    "i.quantity": "Quantity"
    "i.subtotal": "Subtotal"
    "i.rate": "Discount"
    "i.discount": "Discount amount"
    "i.total": "Total"
    "i.vat": "VAT at 20%"
    "i.grand": "Grand total"
    "i.monthly": "Monthly instalment over 12 months"
  }

  field unit_price {
    type = number
    label = t("i.unit")
    default = 19.99
  }

  field quantity {
    type = integer
    label = t("i.quantity")
    default = 3
  }

  field subtotal {
    type = number
    label = t("i.subtotal")
    value = unit_price * quantity
  }

  field discount_rate {
    type = number
    label = t("i.rate")
    default = 0.075
  }

  field discount {
    type = number
    label = t("i.discount")
    value = round(subtotal * discount_rate, 2)
  }

  field total {
    type = number
    label = t("i.total")
    value = subtotal - discount
  }

  field vat {
    type = number
    label = t("i.vat")
    value = round(total * 0.2, 2)
  }

  field grand_total {
    type = number
    label = t("i.grand")
    value = total + vat
  }

  field monthly {
    type = number
    label = t("i.monthly")
    value = round(grand_total / 12, 2)
  }

  output {
    unit_price: number
    quantity: integer
    subtotal: number
    discount: number
    total: number
    vat: number
    grand_total: number
    monthly: number
  }
}

Live form