Triggers and views
Run the right rules at the right moment and in the right place of a form.
A form evaluates rules many times: when the page opens, when a value changes, when a field loses
focus, and on submit. A rule's triggers list the moments it runs on the client. Its target can
also name a place in the user interface: a page, screen, section and component.
A request can carry a trigger and a view. On the client channel, only rules whose triggers
contain the trigger run. On both channels, a rule that names a place runs only when the view names
the same place, or does not name that level. The finding reports the place as its location.
Syntax
- id: <rule id>
kind: validation
target: { entity: Order, page: checkout, section: contact, component: <id>, field: /email }
triggers: [blur, submit] # load | change | blur | submit; default [submit]
# ...{ "entity": "Order", "operation": "create", "trigger": "blur",
"view": { "page": "checkout", "section": "contact" }, "data": { } }Example
The user leaves the e-mail field. Only the contact section is checked, so the 50 items in the basket are not reported yet.
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.triggers, version: 1.0.0, title: Triggers and views }
scope:
- { level: organization, id: learn }
entities:
Order:
schema: { $ref: "./learn.openapi.yaml#/components/schemas/Order" }
rules:
- id: order.email.present
kind: validation
target: { entity: Order, page: checkout, section: contact, field: /email }
operations: [create]
triggers: [blur, submit]
assert: { op: exists, args: [{ var: data.email }] }
severity: error
finding: { code: LRN-TRG-001, message: order.emailMissing }
- id: order.quantity.max
kind: validation
target: { entity: Order, page: checkout, section: cart, component: basket, field: /quantity }
operations: [create]
triggers: [change, submit]
assert: { op: lte, args: [{ var: data.quantity }, 10] }
severity: error
finding: { code: LRN-TRG-002, message: order.tooMany }
- id: order.status.closed
kind: validation
target: { entity: Order, page: checkout, screen: summary, field: /status }
operations: [create]
triggers: [load, submit]
assert: { op: ne, args: [{ var: data.status }, "closed"] }
severity: warning
finding: { code: LRN-TRG-003, message: order.closed }
messages:
en:
order.emailMissing: "Enter an e-mail address."
order.tooMany: "You can order at most 10 items."
order.closed: "This shop is closed today. You can still place the order."
tests:
- name: leaving the e-mail field checks only the contact section
entity: Order
operation: create
channel: client
given:
data: { quantity: 50, status: "closed" }
trigger: blur
view: { page: checkout, section: contact }
expect:
decision: deny
findings:
- { rule: order.email.present, location: { page: checkout, section: contact } }
- name: changing the quantity checks only the basket
entity: Order
operation: create
channel: client
given:
data: { quantity: 50, status: "closed" }
trigger: change
view: { page: checkout, section: cart }
expect:
decision: deny
findings:
- { rule: order.quantity.max, location: { page: checkout, section: cart, component: basket } }
- name: opening the page shows the closed warning
entity: Order
operation: create
channel: client
given:
data: { quantity: 1, status: "closed" }
trigger: load
expect:
decision: allow
findings:
- { rule: order.status.closed, severity: warning }
- name: the server ignores triggers and runs every rule
entity: Order
operation: create
given:
data: { quantity: 50, status: "closed" }
trigger: blur
expect:
decision: deny
findings:
- { rule: order.email.present }
- { rule: order.quantity.max }
- { rule: order.status.closed }{
"entity": "Order",
"operation": "create",
"data": {
"quantity": 50,
"status": "closed"
},
"trigger": "blur",
"view": {
"page": "checkout",
"section": "contact"
}
}Result, from the engine
Decisiondeny1 finding, client channel
LRN-TRG-001errorblockingEnter an e-mail address./email
Open it in the playground and try the other golden tests: change in the cart section, and load
when the page opens. The server ignores trigger and always runs every rule that fits the view.
Common mistakes
- Forgetting
submit. A rule withtriggers: [blur]does not run on submit. Keepsubmitin the list unless you really mean it. - Expecting triggers to filter on the server. Triggers are a client concept. The server always evaluates at the operation itself.
- Free-form place names. Page, screen, section and component ids are lower-case kebab-case,
like
checkoutororder-summary. They are logical names, not framework class names.
Exercise
Check the quantity as soon as the user leaves the quantity field, not only when it changes. Prove
it with a client test: trigger blur in the cart section with 50 items.
Hint
Add blur to the triggers of order.quantity.max.
Show answer
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.triggers, version: 1.0.0, title: Triggers and views }
scope:
- { level: organization, id: learn }
entities:
Order:
schema: { $ref: "./learn.openapi.yaml#/components/schemas/Order" }
rules:
- id: order.email.present
kind: validation
target: { entity: Order, page: checkout, section: contact, field: /email }
operations: [create]
triggers: [blur, submit]
assert: { op: exists, args: [{ var: data.email }] }
severity: error
finding: { code: LRN-TRG-001, message: order.emailMissing }
- id: order.quantity.max
kind: validation
target: { entity: Order, page: checkout, section: cart, component: basket, field: /quantity }
operations: [create]
triggers: [change, blur, submit]
assert: { op: lte, args: [{ var: data.quantity }, 10] }
severity: error
finding: { code: LRN-TRG-002, message: order.tooMany }
- id: order.status.closed
kind: validation
target: { entity: Order, page: checkout, screen: summary, field: /status }
operations: [create]
triggers: [load, submit]
assert: { op: ne, args: [{ var: data.status }, "closed"] }
severity: warning
finding: { code: LRN-TRG-003, message: order.closed }
messages:
en:
order.emailMissing: "Enter an e-mail address."
order.tooMany: "You can order at most 10 items."
order.closed: "This shop is closed today. You can still place the order."
tests:
- name: leaving the quantity field now checks it
entity: Order
operation: create
channel: client
given:
data: { quantity: 50 }
trigger: blur
view: { page: checkout, section: cart }
expect:
decision: deny
findings:
- { rule: order.quantity.max, location: { page: checkout, section: cart, component: basket } }
- name: leaving the e-mail field still skips the basket
entity: Order
operation: create
channel: client
given:
data: { quantity: 50 }
trigger: blur
view: { page: checkout, section: contact }
expect:
decision: deny
findings:
- { rule: order.email.present }{
"entity": "Order",
"operation": "create",
"data": {
"quantity": 50
},
"trigger": "blur",
"view": {
"page": "checkout",
"section": "cart"
}
}Result, from the engine
Decisiondeny1 finding, client channel
LRN-TRG-002errorblockingYou can order at most 10 items./quantity