The screen is a document.
Nova draws an app from documents. An action is its data, a layout over that data, and the triggers that change it. Your own components do the drawing. Because the screen is a document, it can be written, checked and replaced while the app runs.
pnpm add @niscorp/nova @niscorp/prism @niscorp/strata zod · 0.5.1 · Apache-2.0
A ticket, running, and the document it is drawn from
The ticket’s layout
{
component: 'Stack',
props: { gap: 'tight' },
children: [
{ component: 'Title', children: '$.show' },
{ component: 'Text', children: '{{$.seats}} seats' },
{
component: 'Button',
ref: 'more',
children: 'One more',
},
],
}
The Tempest
2 seats
Press “One more” a few times, then change the document. The screen is drawn again from what you chose, and the count stays where you left it: nothing was reloaded, and no component was written. A model can make the same change, and Nova checks it first.
First steps →Four parts, one document.
An action is the unit of an app. This one is a booking, whole: there is no other file behind it.
data is what the action holds when it opens. Everything else reads it or changes it.
{ show: 'The Tempest', seats: 2, total: 0 }layout is a tree of your components, with bindings into the data. It is resolved, never run.
{
component: 'Stack',
children: [
{
component: 'Text',
children: '{{$.seats}} seats for {{$.show}}',
},
{ component: 'Text', children: '{{$.total}} euros' },
{
component: 'Button',
ref: 'more',
children: 'One more',
},
{
component: 'Button',
ref: 'price',
children: 'Price it',
},
],
}triggers answer an event on a ref with steps: here one changes the data, and one makes a call.
[
{
event: 'ui:click',
ref: 'more',
do: [{ increment: 'seats' }],
},
{
event: 'ui:click',
ref: 'price',
do: [{ call: 'quote' }],
},
]endpoints are the calls an action can make, by name: what is sent, and where the answer goes.
{
quote: {
url: '/api/quote',
method: 'POST',
request: { seats: { $ref: '$.seats' } },
response: { $ref: '$.total' },
target: 'total',
},
}Three ways to read the data.
A layout runs no code. It points into the action’s data, and Nova resolves each node before your component sees it. Three nodes, read against the same data.
{ show: 'The Tempest', seats: 2, name: '' }A string that starts with $. is a path. The value at it is put in its place.
{ component: 'Text', children: '$.show' }The Tempest
A string with {{…}} in it is a template. Each path in it is filled in.
{ component: 'Text', children: '{{$.seats}} seats' }2 seats
A node with if draws one branch or the other. An empty name is not there, so it is the second.
{
if: '$.name',
then: { component: 'Text', children: 'For {{$.name}}' },
else: { component: 'Text', children: 'For nobody yet' },
}For nobody yet
One tree, any surface.
Nova’s core resolves a layout to a plain tree, with no framework in it. An adapter turns that tree into something on a surface, with your components, and the same events come back from every one. Five ship in the box. An adapter is a walker over the tree and a kit of about fifteen plain components, so one for another surface is a folder beside these.
Found out before it is pressed.
A document can come from a model. It can parse, mount and draw, and still be broken at the press. auditAction reads a document against itself: every ref against the triggers, every call against the endpoints, every binding against the data.
The booking, as a model might hand it over
{
id: 'booking',
data: { show: 'The Tempest', seats: 2, total: 0 },
layout: {
component: 'Stack',
children: [
{
component: 'Text',
children: '{{$.seats}} seats for {{$.show}}',
},
{ component: 'Text', children: '{{$.discount}} off' },
{
component: 'Button',
ref: 'price',
children: 'Price it',
},
{
component: 'Button',
ref: 'refund',
children: 'Refund',
},
],
},
triggers: [
{
event: 'ui:click',
ref: 'price',
do: [{ call: 'charge' }],
},
],
endpoints: {
quote: {
url: '/api/quote',
method: 'POST',
request: { seats: { $ref: '$.seats' } },
response: { $ref: '$.total' },
target: 'total',
},
},
}