Multi-page apps¶
A Lumen app can be more than one screen. Each screen is its own .lmn file in
the app's src/ directory, and the filename is its name. There is no router to
configure and no route table to keep in sync: add a file, get a page.
Add a page¶
Start from an app whose markup lives in index.lmn, then drop a second file
beside it:
index.lmn- the home pagesettings.lmn- reachable assettingsuser.lmn- reachable asuser
Each page file is a complete markup document with its own <root> element:
<root>
<column gap="8">
<label text="Settings" font-size="24"/>
<label text="Nothing to configure yet."/>
</column>
</root>
Styling attributes on that <root> style that page: bg paints behind it,
padding insets it, and a class puts it in reach of your stylesheet. On the
home page <root> is the app's root element, so what you write there sits
behind every page instead of just one.
Multi-page mode switches on as soon as src/ holds more than one .lmn file.
An app with a single main.lmn keeps loading exactly as before.
The home page is src/index.lmn. If there is no index.lmn, Lumen uses the
file named by [app] entry in lumen.toml, then main.lmn, then the first
page it finds.
Link between pages¶
Use an anchor. The href is a page name, not a URL:
Clicking the anchor switches the active page. An <a> is an ordinary element
otherwise, so style it like any other: give it bg, padding, radius, or a
class. See tags reference.
A click handler that calls prevent_default() on the event stops the
navigation, which lets you confirm before leaving a page. See
scripting. On the web it stops the navigation under
[web] navigation = "soft"; under navigation = "hard" the browser has
already started loading the next document by the time a handler runs.
Navigate from a script¶
The four navigation calls are:
page(path)- go to a pagepage()- the name of the active pagepage_back()- one step back through visited pagespage_forward()- one step forward
Lua uses the same names. In candela they live in the lumen namespace, and the
reader is spelled out because candela has no arity overloading:
lumen::page(path), lumen::page_current(), lumen::page_back(),
lumen::page_forward(). Per-host details are in the
candela,
Rhai, and
Lua references.
Back and forward walk an in-memory history of the pages visited in this run. Navigating to a new page after going back discards the entries ahead of it.
On the web under [web] navigation = "soft" the history is the browser's own,
so page_back() and the browser's back button are the same button, and every
page a visitor reaches has an address they can reload or share. Under
navigation = "hard" the swap still happens but the address does not follow
it, so a page a script navigated to is not one a visitor can reload or share.
See the web target.
Paths with parameters¶
Lumen never pattern-matches a path against :id placeholders. A requested path
resolves to the longest page name that matches its leading segments, and
whatever is left over is handed to the page as text.
With only user.lmn present, navigating to user/42 mounts user.lmn and
leaves /42 for the page to read:
Parse the segment yourself; the framework does not turn it into typed parameters.
Two values are always readable:
route.path- the name of the active pageroute.segment- the leftover path after the page name, empty when there is none
Bind them with bind-text, or gate on them with <if> the same way you would
any other signal. See reactivity.
A path that matches no page at all falls back to the home page with the whole
requested path in route.segment, so you can render your own not-found screen
there.
What every page shares¶
Styling and scripting are app-wide, not per page.
- One stylesheet.
src/main.cssstyles the whole app; split it with@importif it grows. See styling. - One script program per language. The
<script>sources from every page are grouped by host and combined, so define each function once across the app rather than once per page.on_startruns once at startup, not on every navigation. See scripting for how a file picks its host. - Window settings live on the home page.
skin,frameless, and a<menubar>are read from the home page's<root>; the same attributes on another page are ignored.
A shared header, nav bar, or frame belongs in a <template>. Put it in
layout.lmn and every page can use it; see
composition.
Where a page sits¶
A page is not spliced straight into <root>. Each one mounts inside its own
box, and every page's box covers the same rect: the whole of <root>, which
is the whole window. A shell that asks for the full window gets it:
<root>
<column height="100%">
<row height="56"/>
<column grow="1"/>
<row height="40"/>
</column>
</root>
height: 100% measures against the page box, which measures the window, so
the bottom row sits on the bottom edge of the window rather than below it, and
grow on the middle column takes whatever is left between them. Nothing here
needs position: absolute.
The box is what a page's own <root> styles: bg paints it and padding
insets it, so a page's breathing room goes on its own <root>. The box covers
the window edge to edge either way; padding on the home page's <root> does
not inset a page.
The home page's <root> is the app's root element, so its attributes apply
under every page rather than on one page's box.
Page state¶
Navigating away despawns the old page's elements and spawns the new page's from scratch. Anything you want to survive a navigation belongs in a signal rather than in the widget tree.
Configuration¶
Pages need no configuration. When you want to override the defaults, lumen.toml
takes a [pages] block:
entry picks the home page, enabled forces multi-page mode on or off, and
include replaces directory discovery with an explicit list. Full key
descriptions are in the lumen.toml reference.
include paths are relative to src/ and may point into a subdirectory, which
is the way to keep pages in a folder of their own:
A page's name is still its filename stem, so pages/settings.lmn is reachable
as settings. Discovery itself never looks inside a subdirectory: a .lmn
file down there is a page only when include names it, and layout.lmn is
picked up from src/ only. An include naming more than one page switches
multi-page mode on by itself; enabled = false still turns it off.
You can write the whole list before the pages exist. An entry with no file
behind it yet is skipped with a warning naming the path, and the app runs on
the pages that are there; write the file and it joins the set on the next
load. A page that is not there does not count toward the multi-page default
either, so set enabled = true to keep multi-page mode on while the rest are
still to come.
During development¶
lumenc run watches every page file. Editing any page, or the shared
layout.lmn, reloads the app in place.
Shipping a multi-page app¶
lumenc package compiles every page into the app executable, together with the
page names navigation resolves against, so a packaged app routes exactly as it
does here while carrying no .lmn files at all. lumenc build bakes the same
page set into an artifact. Nothing about a page changes once it ships:
page(), <a href>, back and forward, and paths with parameters all behave as
described above.
The one difference is that a shipped app has no page files to reload, so adding a page means rebuilding. See packaging.
On the web the model is the same one it was designed for. lumenc web emits
each page as its own HTML document, and each <a href> becomes an ordinary
link to it, so a page has a URL a visitor can share and a crawler can index. A
path with parameters keeps the shape you wrote: /user/42 stays /user/42,
and route.segment reads the leftover part in the browser exactly as it does
here. See putting an app on the web.