Skip to main content

Build your first Studio app

Goal​

Build a working application from nothing: a table to hold the records, a screen to show them, a toolbar that acts on the one you tick, and a drawer that writes a new record back.

It takes about forty minutes the first time. Nothing here is a toy — every step is the step you would take for a real app, and what you end up with is roughly the Support desk sample.

Read the finished thing first

studio-starter-app in samples/solutions is this app, drawn. Import it from Solutions → Import, open it, and press things. Coming back here after that makes every step below shorter.

Before you start​

  • You are signed in as a maker.
  • An environment and a solution are selected in the pickers at the top of the page. Nothing you make below exists outside them.
  • You know your solution's publisher prefix — the two or three letters in front of every logical name. This page writes par; yours may differ.

1. A table to hold the records​

An app that shows nothing is a drawing. Make the table first.

Tables → New, and fill in:

FieldValue
Display nameCase
Plural nameCases
Logical namepar_case (the prefix is added for you)
Primary fieldSummary

Save, then add three more fields on the Fields tab:

Display nameLogical nameType
Prioritypar_priorityChoice — new option set par_case_priority, options Low, Normal, High
Urgentpar_urgentYes/No
Notespar_notesMultiline text
A Yes/No field starts as nothing, not as No

A record created without touching Urgent holds no value at all for it — and no value is not a boolean. So an expression like record.par_urgent ? "Yes" : "No" fails with Only booleans are allowed in this expression on exactly those records. Write (record.par_urgent ?? false) wherever you read an optional field, and the question goes away. This is the single most common surprise in a first app.


2. The app​

Studio apps → New:

FieldValue
Display nameSupport desk
Logical namepar_support
DescriptionA sentence. It is what the launcher tile shows under the name.
IconPress the picker and choose one — LifeBuoy suits a support app
Listed in the Shellon

Press Save, then Design. The canvas opens.


3. The screen, and what a screen is​

Draw one frame that fills the canvas — about 1200 × 760 is a comfortable drawing size — and, with it selected, open the Screen tab in the inspector on the right:

┌─ Inspector ───────────────────────┐
│ Graphic │ Screen │ Code │
├───────────────────────────────────┤
│ This object is … │
│ ┌─────────────────────────────┐ │
│ │ The screen ▾ │ │ ← choose this
│ └─────────────────────────────┘ │
│ │
│ Content that does not fit │
│ ┌─────────────────────────────┐ │
│ │ Show it — it spills out ▾ │ │
│ └─────────────────────────────┘ │
└───────────────────────────────────┘

Rename the frame Cases in the Layers panel. That name is the screen's name — it is what navigate("Cases") will ask for later.

The size you drew is not the final size. Everything inside is pinned to the frame's edges, and the frame fills whatever window the app is opened in.


4. The screen's own memory​

A screen remembers things: which page of the list you are on, what was typed in the search box, which rows are ticked. That memory is screen state, and it is declared once on the screen frame.

With the screen frame selected, on the Screen tab, add these under State:

NameStarts as
cases[]
caseTotal0
page0
pageSize20
picked""
pickedCount0
pickedId""
form{ par_name: "", par_urgent: false, par_notes: "" }

Declare the shape of form, do not leave it {}. A binding that reads a member an object does not have raises an error, and the screen's first evaluation runs before anything has filled the form in.


5. Fill the list​

Still on the screen frame, on the Screen tab, find Handlers and put this in Loaded:

loadRecords("par_case", "cases", {
top: getState("pageSize"),
skip: getState("page") * getState("pageSize"),
orderBy: "par_name",
countInto: "caseTotal"
});

One line of query is the whole of paging: top and skip are the page, and countInto brings back how many rows there are in all — which a page of twenty cannot say and a Next button has to know. See Show a list a page at a time for the rest of it.

Nothing shows yet. There is nothing drawn to show it in.


6. A row, drawn once​

This is the step that surprises people, so it is worth saying plainly: you draw one row, and the platform draws one per record.

  1. Draw a frame inside the screen for the list — full content width, a few hundred pixels tall. Name it Rows. On the Screen tab set This object is → A list, and set Content that does not fit → Scroll to it, then Which way it scrolls → Down. A box drawn to hold six rows holds a hundred the moment a real table is behind it.

  2. Give it auto layout, vertical, on the Graphic tab. Without it every copy of the row lands on top of the first.

  3. Draw one frame inside Rows, full width, 64 tall. Name it Case row, and set This object is → A list item.

  4. With Case row selected, find Repeat for each on the Screen tab and type:

    state.cases
  5. Draw a text object inside the row for the summary. Select it, and in Bindings set Text:

    row.par_name

row is this copy's record. Save, and press Preview: one row per case, from the table, with nothing repeated by hand.

┌─ Rows ────────────────────────────────────────────────┐
│ ☐ 1 Sample case 1 Normal │ ← Case row, drawn once
│ ☐ 2 Sample case 2 High Urgent │
│ ☐ 3 Sample case 3 Low │
│ … │
└───────────────────────────────────────────────────────┘

7. The tick, and one idea of what is chosen​

Draw a small frame at the start of the row, 20 × 20, and set Control → Tick box. Then:

  • Bindings → Value: state.picked.includes(row.id)

  • Handlers → Changed:

    var ids = "";
    var count = 0;
    var firstId = "";
    var i = 0;
    for (i = 0; i < state.cases.length(); i = i + 1) {
    var id = state.cases[i].id;
    var on = state.picked.includes(id);
    if (id == row.id) { on = !on; }
    if (on) {
    ids = ids + id + "|";
    count = count + 1;
    if (count == 1) { firstId = id; }
    }
    }
    setState("picked", ids);
    setState("pickedCount", count);
    setState("pickedId", firstId);

The value is a comparison, not a field. Every tick re-reads the one list, so clearing the list unticks all of them at once — which a per-row flag could never do, because nothing would know where the other rows were.


8. A toolbar that wakes up​

Draw a bar above the list — a frame, full width, 56 tall, This object is → A region — and put three buttons in it. For each one: draw a frame, set Control → Button, and draw a text object inside it for the caption.

Put a glyph on each. Select the button, then use the icon picker on the Graphic tab — Plus, Pencil, Trash2. What lands is an ordinary drawn path inside the button, and the finished button lays the glyph, the gap and the caption out itself.

ButtonBindings → EnabledHandlers → Clicked
New(leave empty)setState("form", { par_name: "", par_urgent: false, par_notes: "" });
openEndDrawer("New case");
Editstate.pickedCount == 1openStartDrawer("Edit case");
Deletestate.pickedCount > 0showModal("Confirm delete");

Bind Enabled rather than writing code to grey things out. A binding re-evaluates every time the state changes, so the bar wakes up and goes quiet on its own.

Paint the Delete button's text and border in the color.error token, not in a red you picked. A token follows the reader's colour mode; a hex is right for whoever drew it and wrong for everybody whose theme differs.


9. The drawer, and what an overlay card is​

An overlay is not a screen. It is a component you tick as a card, and a handler decides where it opens.

  1. Draw the form somewhere clear of the screen — a frame about 420 wide, with a title, the fields and two buttons. It does not matter where on the canvas it sits; a card is compiled at the origin, and only its width is kept.
  2. Select the frame, press Make component, and name it New case.
  3. On the component's panel, tick An overlay card.
┌─ Inspector · Component ───────────┐
│ ☑ An overlay card │
│ Show it with showModal("New │
│ case") … Its drawn width is │
│ the panel's width. │
│ │
│ ☐ It has to be answered │
│ Clicking outside, Escape and │
│ the close button all stop │
│ working while it is open. │
└───────────────────────────────────┘

Inside it, for each field: draw a frame, set Control → Text box (or Text area for the notes), and bind Value to the state path it edits — state.form.par_name, state.form.par_notes. A control bound to a plain state path writes back to it, so there is nothing else to wire.

Then the Save button's Clicked:

if (getState("form").par_name == "") {
notifyError("A case needs a summary.");
} else {
createRecord("par_case", {
par_name: getState("form").par_name,
par_urgent: getState("form").par_urgent,
par_notes: getState("form").par_notes
});
notify("Case created.");
closeEndDrawer();
setState("page", 0);
loadRecords("par_case", "cases", { top: getState("pageSize"), skip: 0,
orderBy: "par_name", countInto: "caseTotal" });
}

Every write ends by re-reading the list. Nothing refreshes on its own — the screen shows what its state holds, and the state holds what the last query put there.

A question that has to be answered​

Draw the delete confirmation the same way, tick An overlay card, and then tick It has to be answered as well. Clicking outside it, pressing Escape and the close button all stop working — so draw your own way out, a Cancel button running closeModal().

Leave that second tick off everywhere else. An overlay somebody opened by accident should close the way it opened; the only one that should refuse is the one where clicking beside it is not an answer.


10. Save, and open it for real​

Press Save. Warnings appear at the top if the compiler noticed something — a button with no words in it, a screen frame that is missing, a binding that is not a pure expression. They name the object, so fix them by selecting it.

Then leave the designer and open the app from the Shell's app list. Preview is not the same thing: it renders the same screens with no environment behind them, so what it cannot show you is your data. Everything else — the drawers, the tick boxes, the toolbar — behaves identically.


What to do next​

  • A second screen. Draw another frame beside the first, tag it The screen, and name it. An Opens at picker appears in the title bar as soon as there are two. navigate("Board") moves between them.
  • Tabs. A tab strip is not a control: it is one state field and a panel per value. Draw the panels in the same rectangle, bind each one's Shown to state.tab == "notes", and give the tab buttons a Selected binding on the same comparison. Use Draw on this one alone on a layer's row to reach a panel underneath the others.
  • Controls you did not draw. The studio-component-library sample ships a Dropdown, a Lookup and a Toggle. Tick Publishes components on that app and they appear in your palette — see Share a component across screens.
  • A dashboard. The same canvas draws one: see A dashboard on a screen.

Troubleshooting​

What you seeWhat it is
Only booleans are allowed in this expressionAn optional field read as a condition. Wrap it: (state.x.par_urgent ?? false).
A row's binding reads as emptyrow only exists inside the element that carries Repeat for each and its children.
Rows pile up on top of each otherThe box around the repeated row needs auto layout.
Text runs out of the bottom of a boxPut it in a frame set to Scroll to it → Down, and anchor the words to the top.
A dropdown closes the drawer it was opened fromFixed in 0.31. Update, or move the picker out of the drawer.
Save is disabledNothing has changed since the last save.