Views
The designer's HTML is cloned
A screen contains no HTML. It clones the parts it needs from the HTML the designer wrote and fills in values.
Screen >> rowOf: o =
(Html clone: '#lines tr' fill: {
'.c-id' -> o id.
'.c-customer' -> o customer.
'.c-total' -> (self yen: o total) })
key: o id.
#lines tr is a sample row the designer placed in the HTML to see how the table looks. The program clones it for each record and fills the .c-id and other elements.
If the designer adds a column, the program does not change. If a class name changes, the build fails. The module declares its HTML file with page:, and the compiler reads that file and checks that every selector in the program exists in it.
module Orders page: 'index.html'.Screens with many fields
You do not need one line per field. If the JSON keys and the HTML class names follow a rule, the fill can be written as a loop.
Screen >> view = self reply: (
data keys inject: (Html clone: '#card') into: [:node :k |
node at: ('.f-' , k) put: ((data at: k) asStr orElse: [(data at: k) asText]) ]).
This is three lines regardless of the number of fields.
The same applies to the record being edited. In self with: { qty: 120 }, the field name is written in the code, so a misspelling is a compile error. Input from a field cannot be written this way, because which field was edited arrives as a string in the event. For this case there are fieldAt: and fieldAt:put:, which read and write a field by name, and the class answers fieldNames. Values are passed as Json.
Screen >> typed: text at: field =
self with: { vm: (vm fieldAt: field put: (Json str: text)) }.
A field's type is determined by its initial value. If the class declares qty: 0, qty is an Int, and passing "120" to fieldAt: 'qty' put: stores the Int 120. A screen with three hundred fields has one input handler. Order entry is written this way.
No re-render conditions
In many frameworks, you write conditions such as "redraw this part only when this value changes." LPScript has no such conditions.
A handler returns the next state. The runtime compares the returned state with the current one by object identity. Values are immutable, so a state built with with: is always a different object and self is always the same object.
Screen >> search: text = self with: { query: text }. "different object: redraw"
Screen >> ping = self. "same object: no redraw"
Object identity also determines how much of a render is walked. clone:fill: rebuilds only the parts a selector reached, so a row that was not touched is the same node object as in the previous render. Comparison stops there; attributes and children are not compared. On a 1,000-row table where one cell changes, measured over forty renders, one render went from 4.9ms to 3.1ms.
Three things to know:
- Return
selfwhen the state does not change. Nothing is redrawn. - A child actor owns its own region. When the parent redraws, the child is not redrawn if the parent's reference to it is unchanged. Making list rows child actors keeps the parent's redraw out of the rows.
Html child: rowrenders the child inside adiv;Html child: row as: 'tr'specifies the host element. - Give list rows a
key:. Nodes are moved instead of rebuilt, so input fields inside a row keep their content.
Measurement: a game's score label was sent the same string every frame. The label returned a new state each time, so the same content was written to the DOM sixty times a second, and the worst frame gap was 48.2ms. Changing the label to return self when the string is unchanged brought it to 18.8ms. The change was one line.
Comparison with other frameworks
React uses React.memo, useMemo, useCallback, and dependency arrays. Omitting one causes either wasted renders or stale values, and neither is detected until run time. Elm's Html.Lazy serves the same purpose. LPScript has no equivalent of these.
Events are specified by handler name
A click handler is specified by name, not by a block.
(row on: #click send: #select: with: o id)
If the actor's class has no #select: handler, it is a compile error. The value an event carries is typed. Use on:sendValue: to receive the value, or on:sendEvent: to receive the event and read e key, e value, e scrollTop, and so on.
An actor can also receive events from elements outside its region.
me listen: '#back' on: #click send: #goBack.
Blocks are not allowed because a VNode may be used by an actor other than the one that built it: a parent composes views built by child actors, and shared components build views for screens. A block refers to variables in the scope where it was created, so it cannot be copied to another actor. A handler name is a string, so it can. When the event fires, the runtime sends a message with that name to the actor that owns the region.
Keys
When building a row, pass a value that identifies it, such as an order number, with key:.
(Html clone: '#lines tr' fill: { ... }) key: o id
With a key, sorting or filtering moves the DOM node instead of rebuilding it. Input fields inside the row keep their text and focus.
Without a key, the display is still correct: children are matched by position and their content is overwritten. If no row holds input state, this is sufficient.
Two siblings with the same key is an error. Silently repairing it would drop a row without reporting it.
Virtual scrolling
Render time is proportional to the number of rows on screen. Past a few thousand rows, draw only the visible rows. This is a way of writing a screen, not a language feature.
The scroll event carries scrollTop and height. The row height is a constant fixed by the CSS, so dividing gives the range of rows to draw. The height of the rows not drawn goes into spacer rows above and below, which keeps the scrollbar the right length.
Ten thousand rows is written this way. With 10,000 rows, about two dozen are drawn, and one render takes 4.9ms.
Files
Choosing a file is an event. Unlike other events, it does not send its message from the listener immediately.
(node at: '.csv' on: #change sendFileText: #loaded:text:)
Reading starts at the listener, and the handler receives the file name and contents when reading finishes. This is because reading is asynchronous, and the event is over by the time the contents are available. For files that must not be decoded as text, such as reports and spreadsheets, use sendFileBytes:.
File selection, paste, and drag-and-drop are handled the same way. on: #paste and on: #drop reach the same handler.
To hand a file to the user, use App save: 'orders.csv' type: 'text/csv' text: s. The browser's own method (create a link, click it, discard it, revoke the object URL) is four steps, and forgetting the last one leaks memory. This is one message.
Animation
Redrawing every frame uses the same mechanism. Declare requestAnimationFrame as an Extern and have the callback send a message to the actor.
Screen >> tick = (
Win requestAnimationFrame: [:t | me ref tell tick].
...compute the next state... ).
As with any handler, returning the next state redraws the region. Measured at 60 frames per second, with the actor redrawing on every frame.
Drawing to a canvas does not use view. A canvas keeps what was drawn, so the actor clears and redraws it every frame. See Games.