Skip to main content

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.
KeyTypeDefaultDescription
defaultStateAny JSONThe state before anything is stored. Required where there are mutations.
descriptionString or FunctionWhat the state is.
mimeString"application/json"The media type the state is stored as.
queriesObject{}Operations that read.
mutationsObject{}Operations that change the state.
rulesObject{}Limits checked on every mutation.

defaultState​

defaultState: "",
defaultState: { amount: 0, was: null },
Stored valueThe state is
nothingdefaultState
an object missing a key defaultState hasthe stored object, with that key filled in from defaultState
anything elsethe 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}`,
A slide calls a query directly, and renders what it returns. Where a query returns a promise, call its

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 isWrite the mutation toExample
an objectchange it in place({ state, input }) => { state.amount = input.amount; }
one valuereturn the new one({ input }) => input.text

The mutation is handed a copy, so changing it in place is safe.

Change the state or return one, never both. Both throws, because the changes in place would be lost. :::

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.

ArgumentQueryMutationComes from
statewhere there is a defaultStatea copythe stored value
inputyesyesthe caller
propsyesyesthe declaration's props
previousnoyesthe state before this mutation
contextyesyesthe host
feedsyesyesthe host
cmsvalsyesyesthe host
envyesyesthe host
credentialsnot implemented yet
renderedyesyesthe 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;
},
WrittenResult
@param {object} args, then args.input, then one tag per keyThe input is checked against those keys. A key in brackets may be left out.
@param {object} args.input aloneAnything is let through.
@param {object} inputNo 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.

In a preso, 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​

Not implemented yet. :::

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
OptionTypeDescription
descriptionString or FunctionRequired. Added to every mutation's description. A function is handed props.
checkFunctionRequired. Handed { state, props }, where state is what the mutation produced.
check returnsResult
true or nothingPasses.
falseRefused with the description.
a stringRefused with that string.
an objectRefused with its message, and the rest as details.
  • The key in rules is the refusal's code.
  • Every rule runs, and every failure is reported.
  • check is synchronous.

Errors​

Throw one of these to refuse a call. Nothing is stored.

ErrorCodeCan retryThrow when
InvalidInputErrorinvalid-inputyesSending something else could work.
NotAllowedErrornot-allowednoNothing 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.