QORMQORM v0.8.4 docs Get started

First Component

Components let you reuse UI structure. A component is a template declared in components within qorm.json; inside the template, {{ prop.x }} reads the properties passed in by the instance. Instantiate it with a node whose type equals the component name.

Declaring a component (qorm.json)

{
  "type": "app",
  "id": "my_app",
  "entry": "main",
  "components": {
    "user_card": {
      "type": "card",
      "style": { "padding": 16, "gap": 4 },
      "children": [
        { "type": "text", "text": "{{ prop.name }}",  "style": { "fontWeight": 700 } },
        { "type": "text", "text": "{{ prop.email }}", "style": { "color": "#8e8e93" } }
      ]
    }
  }
}

Using a component (scene)

The node's type is the component name; properties are written directly on the node as ordinary fields.

{ "type": "user_card", "id": "u1", "name": "Ada", "email": "[email protected]" }

Reusable JSON components rendered on iOS Components declared once in JSON — metric tiles and key/value panels — reused across a screen (examples/uikit).

Slot (filling in child content)

Place a { "type": "slot" } placeholder in the template; the instance's children are filled into it.

"components": {
  "panel": {
    "type": "card",
    "style": { "padding": 16, "gap": 6 },
    "children": [
      { "type": "text", "text": "{{ prop.title }}", "style": { "fontWeight": 800 } },
      { "type": "slot" }
    ]
  }
}

The instance passes children to fill the slot:

{ "type": "panel", "id": "acct", "title": "Account", "children": [
  { "type": "text", "text": "Plan: Pro" },
  { "type": "text", "text": "Seats: 12" }
] }

Passing live data as props

A prop value may be a binding. It is evaluated once in the instance's scope — so {{ state.x }}, {{ item.x }} and route params all resolve — and the result keeps its type: a boolean stays a boolean, a number stays a number, a list stays a list.

{ "type": "stat_card", "id": "cpu",
  "label": "CPU",
  "value":  "{{ state.metrics.cpu }}",
  "warn":   "{{ state.metrics.cpu > 80 }}",
  "series": "{{ state.metrics.history }}" }

Inside the template those props work as real values, not as text:

{
  "type": "column",
  "children": [
    { "type": "text", "text": "{{ prop.label }}: {{ prop.value * 100 }}%" },
    { "type": "text", "if": "{{ prop.warn }}", "text": "High" },
    { "type": "list", "data": "{{ prop.series }}",
      "renderItem": { "type": "text", "text": "{{ item }}" } }
  ]
}

Because a component instance can sit inside a renderItem, this is what makes a component usable as a list-row template — pass {{ item.… }} straight in.

If you prefer to keep props apart from the node's own fields, write them under a nested props object; those keys win over top-level keys of the same name.

{ "type": "stat_card", "id": "cpu", "props": { "label": "CPU", "value": "{{ state.cpu }}" } }

Callback props

An invoke's name may itself be a binding, so a component can accept the action to run as a prop. The name is resolved when the handler is registered, so the button dispatches the real action:

"components": {
  "confirm_bar": {
    "type": "row",
    "children": [
      { "type": "button", "id": "ok",     "text": "{{ prop.okLabel }}", "onPress": { "name": "{{ prop.onConfirm }}" } },
      { "type": "button", "id": "cancel", "text": "Cancel",             "onPress": { "name": "{{ prop.onCancel }}" } }
    ]
  }
}
{ "type": "confirm_bar", "id": "del", "okLabel": "Delete",
  "onConfirm": "deleteItem", "onCancel": "closeDialog" }

Named slots

A template may declare several slots by giving each a name, and the instance attributes each child to one with a slot field. Children with no slot fill the unnamed slot.

"components": {
  "frame": {
    "type": "column",
    "children": [
      { "type": "slot", "name": "header" },
      { "type": "slot" },
      { "type": "slot", "name": "footer", "children": [
        { "type": "text", "text": "No actions" }
      ] }
    ]
  }
}
{ "type": "frame", "id": "f1", "children": [
  { "type": "text",   "text": "Account",  "slot": "header" },
  { "type": "text",   "text": "Body copy" },
  { "type": "button", "text": "Save", "onPress": "save", "slot": "footer" }
] }

A slot's own children are its fallback — they render only when nothing fills that slot, so the frame above shows "No actions" when the instance supplies no footer child. A single unnamed slot still behaves exactly as before, so existing components keep working.

Declaring props and slots

A component can declare the interface it expects. Wrap the template in a definition object: template holds the root node, props declares the properties and slots the named slots.

"components": {
  "metric": {
    "props": {
      "label": "string",
      "value": { "type": "number", "required": true },
      "unit":  { "type": "string", "default": "pts" }
    },
    "slots": { "header": { "required": false }, "body": { "required": true } },
    "template": {
      "type": "column",
      "children": [
        { "type": "slot", "name": "header" },
        { "type": "text", "text": "{{ prop.label }}" },
        { "type": "text", "text": "{{ prop.value }} {{ prop.unit }}" },
        { "type": "slot", "name": "body" }
      ]
    }
  }
}

A prop is declared either by its type alone ("label": "string") or by an object with type, default and required. Types are string, number, boolean, array, object and any.

What a declaration buys you:

({{ prop.unit }} above is pts unless the instance passes one). A default is a literal from the definition, not an expression evaluated in the instance's scope, and it never satisfies required — a required prop is always the instance's job.

literal value that cannot satisfy its declared type is an error, and a key in the instance's nested props object that the component never declared is a warning. Bindings ({{ state.x }}) are only known at render time, so they are never type-checked here.

template's type checker, so {{ prop.label * 2 }} on a string prop is caught.

Declaring nothing keeps the old behaviour: every key is passed through and nothing is checked, so components written before declarations existed are unaffected.

Components in their own file

A component may also live in its own document — by convention components/<name>.json, next to scenes/ and actions/. Use type of "component", put the component name in id, and the same props / slots / template fields apply:

{
  "qorm": "0.1",
  "type": "component",
  "id": "panel",
  "props": { "title": "string" },
  "slots": { "body": { "required": true } },
  "template": {
    "type": "card",
    "children": [
      { "type": "text", "text": "{{ prop.title }}" },
      { "type": "slot", "name": "body" }
    ]
  }
}

Component files and the manifest's inline components map fill the same registry, so an instance uses either the same way. Defining one name twice is a load-time error (the first definition wins, and the manifest is read first).

Besides {"type": "panel"}, an instance may name the component explicitly with ref, which also accepts the canonical component:// form:

{ "type": "component", "ref": "panel", "props": { "title": "Settings" },
  "children": [ { "type": "text", "text": "Body", "slot": "body" } ] }

ref may itself be a binding — "ref": "{{ item.widget }}" — resolved in the instance's scope, which is how one row template picks a different component per item. A bound ref cannot be checked at load time, and a ref that resolves to no component renders an empty container rather than the unknown-node placeholder.

The convention is components/<name>.json, but the split is driven by the document's type, so a component document can live anywhere in the app folder.