State Declarations

FQL state is declared in a special HTML comment in the document head.

<!-- {STATE}
  state = {
    "email": "#email.oninput.target.value",
    "displayName": "$query.name || 'Guest'"
  }
-->

The value after state = is parsed as a Frontbacked state object. JSON-compatible syntax is recommended, but comment state is forgiving: it accepts single quotes, unquoted keys, unquoted FQL values such as $now, and obvious missing commas between object or array entries.

You can also declare the same state object in JavaScript:

Frontbacked.init({
  state: {
    "email": "#email.oninput.target.value",
    "displayName": "$query.name || 'Guest'"
  }
});

Frontbacked checks init({ state }) first. If no state is passed to init, it falls back to the {STATE} comment in the page.

List Selector State

Use state objects to define lists. The markup then points to that state value with f-list="$state.name".

{
  "pendingTransactions": {
    "$list": "$transactions",
    "$where": { "status": "pending", "expiresOn.$gt": "$now" },
    "$order": { "expiresOn": "asc", "createdOn": "desc" },
    "$limit": 20
  },
  "packages": {
    "$list": "$settings.packages"
  }
}

State selector definitions use special keys such as $list, $where, $order, $limit, and $page so they are not confused with ordinary post, settings, or local state data fields.

$list is intentionally a literal string. Values inside $where can still use FQL roots such as $query, $auth, $state, and $now. Quote keys that contain dots, such as "expiresOn.$gt", for portability; the {STATE} comment parser also accepts clear unquoted operator keys such as expiresOn.$gt. Plain keys like source, where, order, limit, and page remain ordinary state data.

<tbody id="pendingTransactions" f="true" f-list="$state.pendingTransactions"></tbody>
<div f-insert-pagination="#pendingTransactions"></div>

State from Imported HTML

When a page uses f-insert or f-replace, imported files can also declare state in a {STATE} comment inside their own <head>.

<!-- /components/account-link.html -->
<!doctype html>
<html>
<head>
  <!-- {STATE}
    state = {
      "accountLabel": "$siteInfo.name || 'Dashboard'"
    }
  -->
</head>
<body>
  <a href="/dashboard" f="true" f-text="$state.accountLabel">Dashboard</a>
</body>
</html>

Frontbacked resolves imports before serving the page. If an imported file is a full HTML page, only its <body> content is inserted, but its head state is merged into the main page's single {STATE} comment. Imported state entries are placed before the main page state entries, so the main page can override a duplicate key.

State from DOM Events

The most common state source is a DOM event.

{
  "email": "#email.oninput.target.value",
  "accepted": "#terms.onchange.target.checked",
  "file": "#avatar.onchange.target.files[0]"
}

The syntax is:

selector.onevent.path.to.value

Examples:

SourceMeaning
#email.oninput.target.valueOn input, read event.target.value.
#terms.onchange.target.checkedOn change, read checkbox checked state.
#form.onsubmitOn submit, pass the event object.
#file.onchange.target.files[0]On change, read the first selected file.

Event names are written as oninput, onchange, onclick, onsubmit, and so on. FQL removes the on prefix when binding the actual DOM event.

Default Values

Put a literal value in parentheses after a state key to set its default.

{
  "terms(true)": "#terms.onchange.target.checked",
  "quantity(1)": "#quantity.oninput.target.value.Number()",
  "displayName('Guest')": "#name.oninput.target.value",
  "coupon(null)": "#coupon.oninput.target.value"
}

Supported default literals are:

LiteralExample
Boolean"terms(true)"
Number"quantity(1)"
String"name('Guest')"
Null"coupon(null)"

The actual state key is the part before the default. "terms(true)" creates $state.terms with an initial value of true.

Multiple Value Sources

Use a comma to declare multiple possible event sources for the same state value.

{
  "terms(true)": "#terms.onchange.target.checked, #termsBtn.onclick.target.invertTermsClick()"
}

This means $state.terms starts as true, then it can be updated by either:

  1. The hidden checkbox change event.
  2. The visible checkbox button click event.

Commas are only treated as source separators at the top level. Commas inside function calls, arrays, objects, strings, or filters stay inside that expression.

Fallback Expressions with ||

Use || when one expression should fall back to another value during resolution.

{
  "displayName": "$state.form.name || $query.name || $params.username || 'Guest'",
  "title": "$state.product.title || $settings.site.name || 'Untitled'"
}

|| is different from comma-separated value sources.

SyntaxPurpose
CommaMultiple events can write to the same state key.
`

Use commas for "these are possible sources for this state." Use || for "show this value, or this fallback, or this final default."

Logical AND with &&

Use && when one value should depend on another value being truthy.

{
  "canSubmit": "$state.form.email && $state.form.terms"
}

&& evaluates from left to right. It returns the first falsy value it finds. If every value is truthy, it returns the last value. It has higher precedence than ||, so this:

{
  "canSubmit": "$state.form.email && $state.form.terms || false"
}

is read like:

{
  "canSubmit": "($state.form.email && $state.form.terms) || false"
}

FQL || is still a first non-null fallback, so false || 'fallback' resolves to false, not 'fallback'. Use && for truthy gating and || for fallback values.

Formatter and Event Functions

Functions used in FQL are normal page functions. If the script is not classic global JavaScript, expose the function on window.

<script>
  function toNumber(value) {
    return Number(value || 0);
  }

  function formatCurrency(value) {
    return "NGN " + Number(value || 0).toLocaleString();
  }

  function invertTermsClick() {
    return !Frontbacked.getState("form.terms");
  }

  window.toNumber = toNumber;
  window.formatCurrency = formatCurrency;
  window.invertTermsClick = invertTermsClick;
</script>

Use functions after a path segment:

{
  "amount": "#amount.oninput.target.value.toNumber()",
  "formattedAmount": "$state.amount.formatCurrency()",
  "category": "#categories.onchange.target.value.load().post().parseResponse()",
  "terms(true)": "#terms.onchange.target.checked, #termsButton.onclick.target.invertTermsClick()"
}

For event bindings, the function receives the selected event value. For a binding like #termsButton.onclick.target.invertTermsClick(), the function can ignore the passed target and return a computed value.

You can pass a value through multiple functions before it is written to state. In #categories.onchange.target.value.load().post().parseResponse(), Frontbacked reads event.target.value, calls load(value), passes that result into post(...), then passes that result into parseResponse(...). The final return value becomes the state value.

Nested State

State objects can be nested.

{
  "form": {
    "name": "#name.oninput.target.value",
    "email": "#email.oninput.target.value",
    "terms(false)": "#terms.onchange.target.checked"
  },
  "canSubmit": "$state.form.email && $state.form.terms",
  "summary": {
    "name": "$state.form.name",
    "email": "$state.form.email",
    "tags": ["$state.form.name", "$state.form.email"]
  }
}

Nested state is declared like a JSON object with extra FQL powers. Keep object keys quoted and FQL expressions as quoted string values when you can; the {STATE} comment parser tolerates common omissions, while Frontbacked.init({ state }) should use normal JavaScript object syntax. Nested paths are read with $state.form.name, $state.form.email, $state.summary.name, $state.summary.tags[0], and so on.

Reading and Writing State from JavaScript

const allState = Frontbacked.getState();
const email = Frontbacked.getState("form.email");
const missing = Frontbacked.getState("some.deep.value.in.an.undefined.object.0.r.an.array");

Frontbacked.setState("form.status", "Saved");
Frontbacked.setState({
  form: {
    loading: false
  }
});

getState(path) returns undefined when the path moves through a missing object or array. It does not throw for missing data.

const deepValue = Frontbacked.getState("some.deep.value.in.an.undefined.object.0.r.an.array");
// deepValue is undefined

It does throw when a path tries to keep reading through a defined non-object value.

Frontbacked.setState("ages", {
  obj: { user1: 24 },
  array: [24]
});

Frontbacked.getState("ages.obj.user1.throws");
// Cannot access property 'throws' of non-object value, '24'

Frontbacked.getState("ages.array[0].throws");
// Cannot access property 'throws' of non-object value, '24'

Frontbacked.getState("ages.array.0.throws");
// Cannot access property 'throws' of non-object value, '24'

Array indexes can be read or written with square brackets or dot indexes. These point to the same state path:

Frontbacked.getState("sample.array[0]");
Frontbacked.getState("sample.array.0");

Frontbacked.setState("sample.array[0]", 1);
Frontbacked.setState("sample.array.0", 2);

setState(path, value) writes one path. setState(object) merges an object into the current state and re-renders FQL bindings.

Full Form State Example

<head>
  <!-- {STATE}
    state = {
      "form": {
        "name": "#name.oninput.target.value",
        "email": "#email.oninput.target.value",
        "password": "#password.oninput.target.value",
        "rememberMe(true)": "#rememberMe.onchange.target.checked, #rememberButton.onclick.target.toggleRemember()",
        "status": "#signupForm.onsubmit.handleSignup()"
      },
      "buttonLabel": "$state.form.status || 'Create account'",
      "welcome": "$state.form.name || 'New user'"
    }
  -->
</head>

This combines input events, a defaulted checkbox state, multiple sources, submit handling, and derived display state.