Stored rows, a form that adds one, row actions that complete and delete, and a list that reloads after every write.
This tutorial builds a task manager with two traits on one page: a list and a form. The tasks are stored in the browser, so the app needs no server.
1entity Task [persistent: tasks, local] {
2 id : string!
3 title : string!
4 done : boolean = false
5 instances [
6 { id: "T-1", title: "Sketch the states", done: true },
7 { id: "T-2", title: "Write the transitions", done: false }
8 ]
9}local keeps the rows in the viewer's browser. instances seeds them the first time the store is created.
The list starts in loading, fetches on INIT, and moves to ready when the rows arrive. The fetch reports through two events, which the trait declares in emits:
1state loading {
2 INIT -> loading
3 (fetch Task { emit: { success: TasksLoaded, failure: TasksFailed } })
4 (render-ui main { type: skeleton, variant: list, rows: 3 })
5 TasksLoaded -> ready
6 (render-ui main { type: data-list, entity: ?data, fields: [{ name: "title", label: "Task", variant: h4 }] })
7 TasksFailed -> ready
8 (render-ui main { type: alert, variant: error, message: ?error })
9}itemActions put buttons on each row. A row action sends its event with { id, row }, and when decides per row whether the button shows:
▼itemActions: [
{ label: "Complete", event: COMPLETE, variant: ghost, when: (fn row (not (object/get @row done false))) },
{ label: "Delete", event: DELETE, variant: danger }
]1COMPLETE -> loading
2 (persist update Task (object/merge ?row { done: true }))
3 (fetch Task { emit: { success: TasksLoaded, failure: TasksFailed } })
4DELETE -> loading
5 (persist delete Task ?id)
6 (fetch Task { emit: { success: TasksLoaded, failure: TasksFailed } })A second trait shows a form-section. Submitting sends SAVE with the form's values as ?data; the trait stores the row and emits TASK_ADDED:
SAVE -> editing
(persist create Task ?data)
(emit TASK_ADDED)The list listens and reloads:
▼listens {
TaskForm.TASK_ADDED -> RELOAD
}TaskList
on Task · states: 2 · transitions: 6TaskForm
on Task · states: 1 · transitions: 2The program above as state machines, one per trait: each box is a state, each arrow an event that moves it, and the icons on an arrow are the effects that event runs. A ◇ on an arrow is a guard; hover it to read the check. How to read AVL
1app std-orb-docex-tasks "1.0.0"
2"std-orb-docex-tasks — a task manager: stored tasks, a form that adds one, row actions that complete or delete, and a list that reloads after every write."
3orbital TasksOrbital {
4 entity Task [persistent: docex_tasks, local] {
5 id : string!
6 title : string!
7 done : boolean = false
8 instances [
9 { id: "T-1", title: "Sketch the states", done: true },
10 { id: "T-2", title: "Write the transitions", done: false },
11 { id: "T-3", title: "Run orb validate", done: false }
12 ]
13 }
14
15 trait TaskList -> Task [interaction, collection] {
16 initial: loading
17
18 state loading {
19 INIT -> loading
20 (fetch Task { emit: { success: TasksLoaded, failure: TasksFailed } })
21 (render-ui main { type: skeleton, variant: list, rows: 3 })
22 TasksLoaded -> ready
23 (emit MOVED { event: "TasksLoaded" })
24 (render-ui main { type: data-list, entity: ?data, fields: [{ name: "title", label: "Task", variant: h4 }, { name: "done", label: "Done", variant: badge, labels: { "true": "Done", "false": "Open" } }], itemActions: [{ label: "Complete", event: COMPLETE, variant: ghost, when: (fn row (not (object/get @row done false))) }, { label: "Delete", event: DELETE, variant: danger }], emptyTitle: "Nothing to do", emptyDescription: "Add a task with the form.", emptyIcon: "check-circle" })
25 TasksFailed -> ready
26 (emit MOVED { event: "TasksFailed" })
27 (render-ui main { type: alert, variant: error, message: ?error })
28 }
29
30 state ready {
31 COMPLETE -> loading
32 (emit MOVED { event: "COMPLETE" })
33 (persist update Task (object/merge ?row { done: true }))
34 (fetch Task { emit: { success: TasksLoaded, failure: TasksFailed } })
35 DELETE -> loading
36 (emit MOVED { event: "DELETE" })
37 (persist delete Task ?id)
38 (fetch Task { emit: { success: TasksLoaded, failure: TasksFailed } })
39 RELOAD -> loading
40 (emit MOVED { event: "RELOAD" })
41 (fetch Task { emit: { success: TasksLoaded, failure: TasksFailed } })
42 }
43
44 emits {
45 TasksLoaded {
46 data : [Task]
47 }
48 TasksFailed {
49 error : string
50 }
51 MOVED -> external {
52 event : string!
53 }
54 @description "Each move names the event it took, so the diagram beside the demo can follow it."
55 }
56
57 listens {
58 TaskForm.TASK_ADDED -> RELOAD
59 COMPLETE {
60 id : Task.id!
61 row : Task!
62 }
63 DELETE {
64 id : Task.id!
65 }
66 }
67 }
68
69 trait TaskForm -> Task [interaction, instance] {
70 initial: editing
71
72 state editing {
73 INIT -> editing
74 (render-ui main { type: form-section, fields: ["title"], submitEvent: SAVE, mode: create, entity: {} })
75 SAVE -> editing
76 (emit MOVED { event: "SAVE" })
77 (persist create Task ?data)
78 (emit TASK_ADDED)
79 (render-ui main { type: form-section, fields: ["title"], submitEvent: SAVE, mode: create, entity: {} })
80 }
81
82 emits {
83 TASK_ADDED
84 @description "A task was stored."
85 MOVED -> external {
86 event : string!
87 }
88 @description "Each move names the event it took, so the diagram beside the demo can follow it."
89 }
90
91 listens {
92 SAVE {
93 data : @entity!
94 }
95 }
96 }
97
98 page "/tasks" -> TaskList, TaskForm
99}What happens when COMPLETE fires:
(persist "update" "Task" (object/merge @payload.row { done: true }))(fetch "Task" { emit: { failure: "TasksFailed", success: "TasksLoaded" } })How the traits talk to each other, in order:
Add a task, complete one, delete one. Reload the page: the tasks are still there, because they are stored in your browser.
UI patterns goes deeper into what a screen can show.
The language where the rule is the program.