defineActions()
defineActions(declaration);
Declares a piece of state, the queries that read it and the mutations that change it. Give the result to a declaration's actions. Write it in the same file as what renders the field, or in its own file and import the result - see actions in the same file for what a query or mutation can use when it stays in that file.
import { defineActions } from "@livepreso/webpack-deck";
export const price = defineActions({
defaultState: { amount: 0 },
description: "A price shown on a slide.",
queries: {
/**
* The price as the slide shows it.
*
* @returns {string} the amount and its currency
*/
formatted: ({ state, props }) => `$${state.amount} ${props.currency}`,
},
mutations: {
/**
* Change the price.
*
* @param {object} args
* @param {object} args.input
* @param {number} args.input.amount the new price
*/
set: ({ state, input }) => {
state.amount = input.amount;
},
},
});
operations published for <Price id="monthly" currency="AUD" /> on the slide "plans"
plans-monthly.formatted query The price as the slide shows it.
plans-monthly.set mutation Change the price. A price shown on a slide.
| Key | Type | Default | Description |
|---|---|---|---|
| defaultState | Any JSON | The state before anything is stored. Required where there are mutations. | |
| description | String or Function | What the state is. | |
| mime | String | "application/json" | The media type the state is stored as. |
| queries | Object | {} | Operations that read. |
| mutations | Object | {} | Operations that change the state. |
| rules | Object | {} | Limits checked on every mutation. |
defaultState
defaultState: "",
defaultState: { amount: 0, was: null },
| Stored value | The state is |
|---|---|
| nothing | defaultState |
an object missing a key defaultState has | the stored object, with that key filled in from defaultState |
| anything else | the stored value |
A key added to defaultState later reaches prices already stored without a migration.
Where defaultState is a string, the copy written in the source is the state before anything is stored.
description
description: "A price shown on a slide.",
description: (props) => `A price in ${props.currency}.`,
Joined onto the description of every query and mutation.
mime
mime: "text/plain",
queries
A query reads.
queries: {
formatted: ({ state, props }) => `$${state.amount} ${props.currency}`,
// Offered only to sessions on these channels.
amount: { channels: ["everyone"], run: ({ state }) => state.amount },
},
The doc comment's first sentence is the query's description, and @returns is what it gives back:
/**
* The price as the slide shows it.
*
* @returns {string} the amount and its currency
*/
formatted: ({ state, props }) => `$${state.amount} ${props.currency}`,
Async() twin from a slide instead - the plain call would render the promise itself, not what it resolves to. :::
mutations
A mutation changes the state, and nothing else.
| The state is | Write the mutation to | Example |
|---|---|---|
| an object | change it in place | ({ state, input }) => { state.amount = input.amount; } |
| one value | return the new one | ({ input }) => input.text |
The mutation is handed a copy, so changing it in place is safe.
The doc comment's first sentence is the mutation's description. See input for describing what it takes.
Arguments
Every query and mutation is handed one object. Destructure what it reads, and only that.
| Argument | Query | Mutation | Comes from |
|---|---|---|---|
| state | where there is a defaultState | a copy | the stored value |
| input | yes | yes | the caller |
| props | yes | yes | the declaration's props |
| previous | no | yes | the state before this mutation |
| context | yes | yes | the host |
| feeds | yes | yes | the host |
| cmsvals | yes | yes | the host |
| env | yes | yes | the host |
| credentials | not implemented yet | ||
| rendered | yes | yes | the deck, while the field is on screen |
Naming a host argument decides where the operation is offered:
inNzd: ({ state, feeds }) => ...
a session with feeds inNzd is offered
a session without feeds inNzd is not offered
state
formatted: ({ state, props }) => `$${state.amount} ${props.currency}`,
A query in a declaration with no defaultState is handed no state.
input
What the caller sent. It is always JSON.
/**
* Change the price.
*
* @param {object} args
* @param {object} args.input
* @param {number} args.input.amount the new price
* @param {string} [args.input.reason] why it changed
*/
set: ({ state, input }) => {
state.amount = input.amount;
},
| Written | Result |
|---|---|
@param {object} args, then args.input, then one tag per key | The input is checked against those keys. A key in brackets may be left out. |
@param {object} args.input alone | Anything is let through. |
@param {object} input | No schema. Every input is refused. |
A function, a class instance or anything else that is not JSON cannot be sent.
props
// declaration: props: { currency: prop("currency") }
formatted: ({ state, props }) => `$${state.amount} ${props.currency}`,
previous
Mutations only. The state as it was before this mutation, since changing state in place overwrites it.
// Keeps the old price, so the slide can show "Was $49".
set: ({ state, previous, input }) => {
state.amount = input.amount;
state.was = previous.amount;
},
context
The preso's Context.
discounted: ({ state, context }) => state.amount * (1 - context.discountRate),
feeds
The deck's feeds.
inNzd: ({ state, feeds }) => state.amount * feeds.exchangeRates.nzd,
cmsvals
The deck's CMSVals.
cmsvals is the copy taken when the preso was made, so a later PresoManager edit does not reach it.listPrice: ({ cmsvals }) => cmsvals["plans-monthly"],
env
The preso's environment values.
taxLabel: ({ env }) => (env.region === "nz" ? "incl. GST" : "incl. tax"),
credentials
rendered
What the deck is holding for the field while it is on screen, under names the deck chooses.
// Whether the formatted price fits its box.
fits: ({ rendered }) => rendered.box.scrollWidth <= rendered.box.clientWidth,
Until the slide is on screen, a call is refused with no-render. Hand the values over with rendered in
useActions(), or provideRendered().
rules
defineRule({ description, check });
A limit on the state, and the sentence a caller reads before writing.
import { defineActions, defineRule } from "@livepreso/webpack-deck";
const notNegative = defineRule({
description: "A price cannot be below zero.",
check: ({ state }) => state.amount >= 0,
});
export const price = defineActions({
defaultState: { amount: 0 },
rules: { "price-below-zero": notNegative },
mutations: {
set: ({ state, input }) => {
state.amount = input.amount;
},
},
});
set({ amount: -5 })
refused price-below-zero "A price cannot be below zero."
stored nothing
| Option | Type | Description |
|---|---|---|
| description | String or Function | Required. Added to every mutation's description. A function is handed props. |
| check | Function | Required. Handed { state, props }, where state is what the mutation produced. |
check returns | Result |
|---|---|
true or nothing | Passes. |
false | Refused with the description. |
| a string | Refused with that string. |
| an object | Refused with its message, and the rest as details. |
- The key in
rulesis the refusal's code. - Every rule runs, and every failure is reported.
checkis synchronous.
Errors
Throw one of these to refuse a call. Nothing is stored.
| Error | Code | Can retry | Throw when |
|---|---|---|---|
InvalidInputError | invalid-input | yes | Sending something else could work. |
NotAllowedError | not-allowed | no | Nothing sent would work. |
import { InvalidInputError, NotAllowedError } from "@livepreso/webpack-deck";
set: ({ state, input, props }) => {
if (props.isFixed) {
throw new NotAllowedError("This price is set by head office.");
}
if (!Number.isInteger(input.amount)) {
throw new InvalidInputError(`Prices are whole dollars, not ${input.amount}.`);
}
state.amount = input.amount;
},
A second argument adds details:
throw new NotAllowedError("This price is set by head office.", { owner: "pricing" });
The build lists the errors each operation throws, so a caller knows them before calling.