Avtomati stanj, ki se odzivajo na dogodke, preverjajo pogoje, izvajajo učinke in rišejo zaslon.
Lastnost je avtomat stanj, vezan na entiteto. Našteje stanja; vsako stanje našteje dogodke, ki jih sprejme, kam vsak vodi in kaj se zgodi na poti.
1trait Approval -> Expense [interaction, instance] {
2 initial: draft
3
4 state draft {
5 SUBMIT -> submitted when (> @entity.amount 0)
6 (render-ui main { type: typography, content: "Submitted", variant: h4 })
7 }
8
9 state submitted {
10 APPROVE -> approved when (<= @entity.amount 500)
11 (set @entity.note "Approved")
12 REJECT -> rejected
13 (set @entity.note "Rejected")
14 }
15}EVENT -> target je prehod. Učinki sledijo v svojih vrsticah in se izvedejo po vrsti.when (…) je pogoj: če ne drži, je dogodek zavrnjen in nič se ne spremeni.initial: navede drugega.Oklepaj v glavi vsebuje kategorijo (interaction, lifecycle, integration, …) in instance (en avtomat za vsako vrstico entitete, privzeto) ali collection (en avtomat za celo entiteto).
Tu teče celoten postopek odobritve. Poskusi oddati zahtevek nad 500 EUR in ga odobriti; pogoj ga zavrne:
Approval
on Expense · states: 4 · transitions: 7Ista lastnost kot avtomat stanj: vsak okvir je stanje, vsaka puščica dogodek, ki ga premakne, ikone na puščici pa so učinki, ki jih ta dogodek izvede. Z miško pojdite čez simbol in videli boste, kaj naredi. Kako brati AVL
1app std-orb-docex-expense "1.0.0"
2"std-orb-docex-expense — an approval flow: four states, a guard on the amount, and a rule the shape of the program enforces."
3orbital ExpenseOrbital {
4 entity Expense [runtime] {
5 id : string!
6 amount : number = 120
7 note : string = ""
8 }
9
10 trait Approval -> Expense [interaction, instance] {
11 initial: draft
12
13 state draft {
14 INIT -> draft
15 (render-ui main { type: stack, direction: vertical, gap: sm, children: [
16 { type: typography, content: (str/concat "Draft claim: " @entity.amount " EUR"), variant: h4 },
17 { type: stack, direction: horizontal, gap: sm, children: [
18 { type: button, label: "Add 50 EUR", action: RAISE, variant: ghost },
19 { type: button, label: "Submit", action: SUBMIT, variant: primary }
20 ] }
21 ] })
22 RAISE -> draft
23 (emit MOVED { event: "RAISE" })
24 (set @entity.amount (+ @entity.amount 50))
25 SUBMIT -> submitted when (> @entity.amount 0)
26 (emit MOVED { event: "SUBMIT" })
27 (render-ui main { type: stack, direction: vertical, gap: sm, children: [
28 { type: typography, content: (str/concat "Submitted: " @entity.amount " EUR"), variant: h4 },
29 { type: typography, content: "Claims over 500 EUR cannot be approved here.", variant: small, color: muted },
30 { type: stack, direction: horizontal, gap: sm, children: [
31 { type: button, label: "Approve", action: APPROVE, variant: primary },
32 { type: button, label: "Reject", action: REJECT, variant: secondary }
33 ] }
34 ] })
35 }
36
37 state submitted {
38 APPROVE -> approved when (<= @entity.amount 500)
39 (emit MOVED { event: "APPROVE" })
40 (set @entity.note "Approved")
41 (render-ui main { type: stack, direction: vertical, gap: sm, children: [
42 { type: badge, label: "Approved", variant: success },
43 { type: button, label: "New claim", action: NEW_CLAIM, variant: ghost }
44 ] })
45 REJECT -> rejected
46 (emit MOVED { event: "REJECT" })
47 (set @entity.note "Rejected")
48 (render-ui main { type: stack, direction: vertical, gap: sm, children: [
49 { type: badge, label: "Rejected", variant: error },
50 { type: button, label: "Revise", action: REVISE, variant: primary }
51 ] })
52 }
53
54 state approved {
55 NEW_CLAIM -> draft
56 (emit MOVED { event: "NEW_CLAIM" })
57 (set @entity.amount 120)
58 (render-ui main { type: stack, direction: vertical, gap: sm, children: [
59 { type: typography, content: (str/concat "Draft claim: " @entity.amount " EUR"), variant: h4 },
60 { type: stack, direction: horizontal, gap: sm, children: [
61 { type: button, label: "Add 50 EUR", action: RAISE, variant: ghost },
62 { type: button, label: "Submit", action: SUBMIT, variant: primary }
63 ] }
64 ] })
65 }
66
67 state rejected {
68 REVISE -> draft
69 (emit MOVED { event: "REVISE" })
70 (render-ui main { type: stack, direction: vertical, gap: sm, children: [
71 { type: typography, content: (str/concat "Draft claim: " @entity.amount " EUR"), variant: h4 },
72 { type: stack, direction: horizontal, gap: sm, children: [
73 { type: button, label: "Add 50 EUR", action: RAISE, variant: ghost },
74 { type: button, label: "Submit", action: SUBMIT, variant: primary }
75 ] }
76 ] })
77 }
78
79 emits {
80 MOVED -> external {
81 event : string!
82 }
83 @description "Each move names the event it took, so the diagram beside the demo can follow it."
84 }
85 }
86
87 page "/expenses" -> Approval
88}Kaj se zgodi, ko se sproži APPROVE:
(<= @entity.amount 500)(set @entity.note "Approved")| Učinek | Kaj naredi |
|---|---|
(set @entity.f v) | Nastavi polje. Edina oblika prirejanja. |
(fetch E { … }) / (persist create|update|delete E data) | Bere in piše shranjene vrstice. |
(emit EVENT { … }) | Odda navedeni dogodek. |
(render-ui slot { … }) | Nariše drevo vzorcev v režo (main, modal, drawer, toast, …); null jo izprazni. |
(navigate "/path") / (navigate-back) | Premik na drugo stran. |
(notify info "Saved") | Pokaže obvestilo. |
(call-service provider action { … }) | Pokliče integracijo (jezikovni model, e-pošta, plačila, …). |
Operatorji računajo vrednosti: (+ a b), (str/concat …), (array/map xs (fn x …)). Vsi so predponski; infiksnih ni. Referenca operatorjev našteje vsakega.
| Znak | Pomen |
|---|---|
@entity.field | Polje vezane entitete |
?field | Polje tovora prihajajočega dogodka |
@config.key | Vrednost iz config lastnosti |
@user.field | Prijavljeni obiskovalec |
now / state | Časovni žig te obdelave / trenutno stanje |
Lastnost navede, kaj oddaja, druga pa, na kaj se odziva. Tu gumb oddaja TARGET_CHANGED, stanje pa ga posluša in preslika v svoj dogodek SHOW:
Dial
on Thermostat · states: 1 · transitions: 3Status
on Thermostat · states: 1 · transitions: 2Set a temperature between 16 and 28 °C.
1app std-orb-docex-thermostat "1.0.0"
2"std-orb-docex-thermostat — two traits in one orbital: one guards and emits, the other listens and reacts."
3orbital ThermostatOrbital {
4 entity Thermostat [runtime, shared] {
5 id : string!
6 target : number = 21
7 }
8
9 trait Dial -> Thermostat [interaction, instance] {
10 initial: idle
11
12 state idle {
13 INIT -> idle
14 (render-ui main { type: stack, direction: horizontal, gap: md, align: center, children: [
15 { type: button, label: "Colder", action: DOWN, variant: secondary },
16 { type: typography, content: (str/concat @entity.target " °C"), variant: h2 },
17 { type: button, label: "Warmer", action: UP, variant: secondary }
18 ] })
19 UP -> idle when (< @entity.target 28)
20 (emit MOVED { event: "UP" })
21 (set @entity.target (+ @entity.target 1))
22 (emit TARGET_CHANGED { target: @entity.target })
23 DOWN -> idle when (> @entity.target 16)
24 (emit MOVED { event: "DOWN" })
25 (set @entity.target (- @entity.target 1))
26 (emit TARGET_CHANGED { target: @entity.target })
27 }
28
29 emits {
30 TARGET_CHANGED {
31 target : number!
32 }
33 @description "The target temperature moved."
34 MOVED -> external {
35 event : string!
36 }
37 @description "Each move names the event it took, so the diagram beside the demo can follow it."
38 }
39 }
40
41 trait Status -> Thermostat [interaction, instance] {
42 initial: watching
43
44 state watching {
45 INIT -> watching
46 (render-ui main { type: typography, content: "Set a temperature between 16 and 28 °C.", variant: body, color: muted })
47 SHOW -> watching
48 (emit MOVED { event: "SHOW" })
49 (render-ui main { type: badge, label: (if (> ?target 23) "Heating up" (if (< ?target 19) "Cooling down" "Comfortable")), variant: (if (> ?target 23) warning (if (< ?target 19) info success)) })
50 }
51
52 emits {
53 MOVED -> external {
54 event : string!
55 }
56 @description "Each move names the event it took, so the diagram beside the demo can follow it."
57 }
58
59 listens {
60 Dial.TARGET_CHANGED -> SHOW
61 SHOW {
62 target : number!
63 }
64 }
65 }
66
67 page "/thermostat" -> Dial, Status
68}Kaj se zgodi, ko se sproži UP:
(< @entity.target 28)(set @entity.target (+ @entity.target 1))(emit "TARGET_CHANGED" { target: @entity.target })1emits {
2 TARGET_CHANGED {
3 target : number!
4 }
5}
6
7listens {
8 Dial.TARGET_CHANGED -> SHOW
9 SHOW {
10 target : number!
11 }
12}Dogodki internal (privzeto) ostanejo v orbitalu; dogodki external prestopijo v druge orbitale in morajo navesti tovor. Trait.EVENT se razreši znotraj orbitala poslušalca; med orbitali pišeš Orbital.Trait.EVENT. Dogodki med orbitali pokažejo oboje.
Tik je periodični učinek. Teče le, dokler drži njegov when, zato se tik, vezan na stanje, ustavi takoj, ko lastnost stanje zapusti:
Timing
on Stopwatch · states: 2 · transitions: 41app std-orb-docex-stopwatch "1.0.0"
2"std-orb-docex-stopwatch — a tick: a periodic effect that runs only while the trait is in the running state."
3orbital StopwatchOrbital {
4 entity Stopwatch [runtime] {
5 id : string!
6 seconds : number = 0
7 }
8
9 trait Timing -> Stopwatch [interaction, instance] {
10 initial: stopped
11
12 state stopped {
13 INIT -> stopped
14 (render-ui main { type: stack, direction: horizontal, gap: md, align: center, children: [
15 { type: typography, content: (str/concat @entity.seconds " s"), variant: h2 },
16 { type: button, label: "Start", action: START, variant: primary },
17 { type: button, label: "Reset", action: RESET, variant: ghost }
18 ] })
19 START -> running
20 (emit MOVED { event: "START" })
21 (render-ui main { type: stack, direction: horizontal, gap: md, align: center, children: [
22 { type: typography, content: (str/concat @entity.seconds " s"), variant: h2 },
23 { type: button, label: "Stop", action: STOP, variant: secondary }
24 ] })
25 RESET -> stopped
26 (emit MOVED { event: "RESET" })
27 (set @entity.seconds 0)
28 }
29
30 state running {
31 STOP -> stopped
32 (emit MOVED { event: "STOP" })
33 (render-ui main { type: stack, direction: horizontal, gap: md, align: center, children: [
34 { type: typography, content: (str/concat @entity.seconds " s"), variant: h2 },
35 { type: button, label: "Start", action: START, variant: primary },
36 { type: button, label: "Reset", action: RESET, variant: ghost }
37 ] })
38 }
39
40 emits {
41 MOVED -> external {
42 event : string!
43 }
44 @description "Each move names the event it took, so the diagram beside the demo can follow it."
45 }
46
47 ticks {
48 second every 1s when (= state "running")
49 (set @entity.seconds (+ @entity.seconds 1))
50 }
51 }
52
53 page "/stopwatch" -> Timing
54}Kaj se zgodi, ko se sproži START:
▼ticks {
second every 1s when (= state "running")
(set @entity.seconds (+ @entity.seconds 1))
}Intervali so milisekunde, trajanje (5s, 1m, 1h, 1d) ali niz cron. Tik na shranjeni entiteti teče na strežniku, dokler ima lastnost odprta neka stran; z oznako [background] teče tudi, ko nihče ne gleda.
config { pageSize : int = 10 @label "Items per page" } navede gumb, ki ga lastnost bere kot @config.pageSize. Gumbi so tisto, kar sme mesto klica spremeniti, ko lastnost uvozi; glej sestavljanje.
Strani postavijo lastnosti na zaslon.