LeCodesdocs

UIScreen & Router

UIScreen is the root of every UI tree — it always fills the device and behaves as a UIColumn (or as a scroll container after .makeScrollable()). A single-screen app calls screen.open(); a multi-page app hands its screens to Router and navigates with push / pop / replace.

At a glance

TypeScript
const detail = UIScreen([
  UIText("Detail").style({ fontSize: 24, fontWeight: 700, color: "white" }),
]).style({ bgColor: "black", p: 20, pt: "max(safe-top, 24px)" })
  .onBackPressed(() => Router.pop())

const home = UIScreen([
  UIText("Home").style({ fontSize: 24, fontWeight: 700, color: "white" }),
  UIButton([ UIText("Open detail").style({ color: "white" }) ])
    .style({ bgColor: "#FF4032", borderRadius: 12, p: 16 })
    .onClick(() => Router.push(detail)),
]).style({ bgColor: "black", p: 20, pt: "max(safe-top, 24px)", gap: 16 })

Router.init(home)

UIScreen

TypeScript
UIScreen(): UIScreen
UIScreen(children: UINodeChild[]): UIScreen

screen.open(): void     // show this screen directly (single-screen apps; hides an active Router)
screen.close(): void    // close the directly-opened screen

Full container surface: .append / .insert / .remove / .setContent, .style, .animateTo / .animateFrom, .onLayout.

Note

a screen always fills the device — sizing styles on it (width, height, flexGrow, flexShrink, position) are no-ops. Style its padding, background, and layout of children. The default background is black; always set bgColor and text colors explicitly.

Lifecycle — onOpen / onClose

TypeScript
screen.onOpen(cb: () => void): this    // screen became the active one
screen.onClose(cb: () => void): this   // screen stopped being the active one

These fire on every activation, not just the first: pushing away from a screen fires its onClose, popping back to it fires onOpen again. Anything started in onOpen must be stopped in onClose — loops, intervals, sockets — or it keeps running behind other screens. See Lifecycle.

TypeScript
let loopId: number | null = null
screen
  .onOpen(() => { loopId = setLoop(dt => tick(dt)) })
  .onClose(() => { if (loopId !== null) { clearLoop(loopId); loopId = null } })

Touch & back button

TypeScript
screen.onTouchStart(ev => …)          // fires for touches anywhere on the screen (full-screen gestures)
screen.onBackPressed(cb: () => void)  // Android hardware/gesture back — typically Router.pop()

onTouchStart supports ev.track() like a button — see Pointer events.

Scrolling — makeScrollable()

A screen is not scrollable by default. makeScrollable() turns the whole screen into one vertical scroll flow and unlocks three methods that don't exist otherwise:

TypeScript
const screen = UIScreen([...])
  .makeScrollable()                          // returns the same screen, now scrollable
  .onRefresh(async () => { await load() })   // pull-to-refresh; spinner hides when the promise resolves
screen.onScroll(pos => { })                  // scroll position in px
screen.onOverscroll(pos => { })              // pull beyond the edge
Note

of the three, only onRefresh is declared chainable (returns the screen); call onScroll / onOverscroll as statements.

refreshControlColor (a screen style) tints the native pull-to-refresh spinner.

Note

pull-to-refresh exists only on makeScrollable() — a child UIScrollable cannot provide it. Rule of thumb: whole screen is one flow (article, feed, long form) → makeScrollable(); a scroll region inside a fixed layout (list under a pinned header) → a child UIScrollable.

Router

TypeScript
Router.init(homePage, opts?: { showDefaultBackButton?: boolean })  // call once in the entry file
Router.push(screen)                    // push onto the stack, screen becomes active
Router.pop(to?: number)                // default -1 = one screen back
Router.replace(screen, opts?: { transition })   // swap the top screen
Router.current                         // the active UIScreen (getter)
Router.hide()                          // hide the whole router (stack stays alive)
Router.restore()                       // bring it back, reactivating the top screen
  • pop(to) — negative values are relative (-2 = back two screens); 0 and positive values are an absolute index into the stack (0 = the home screen). Screens that leave the stack are destroyed.
  • replace transitions: "slide-from-left" | "slide-from-right" | "slide-from-top" | "slide-from-bottom" | "zoom" | "zoom-out" | "zoom-in" | "fade" | "none" — default "fade".
  • init's showDefaultBackButton (default false) shows a host-provided back button.

How screens live on the stack

Screens below the top stay mounted — their element trees and state survive, they're just not rendered. Navigation drives lifecycle callbacks: push fires the outgoing screen's onClose and the incoming one's onOpen; pop fires them in reverse, destroys the popped screens' native trees, and re-fires onOpen on the screen you land on. A popped UIScreen object is still usable — pushing it again remounts it. Router.hide() fires the top screen's onClose; restore() fires onOpen again. Calling screen.open() while a router is active hides the router (its stack stays restorable).

Route-change events

TypeScript
Router.addEventListener("change", (screen: UIScreen) => …)   // fires after every navigation
Router.removeEventListener("change", cb)

The callback receives the newly active screen — useful for a shared tab bar or analytics.

Pitfalls

TypeScript
// ✗ onRefresh / onScroll without makeScrollable()
UIScreen([...]).onRefresh(load)          // method doesn't exist on a plain screen
// ✓
UIScreen([...]).makeScrollable().onRefresh(load)

// ✗ loop started in onOpen, never stopped — keeps running after Router.push()
screen.onOpen(() => setLoop(update))
// ✓ pair it in onClose (fires on every navigation away)

See also