defineEditableSource()
defineEditableSource(callable, declaration);
Declares which fields callable produces and how their keys are built. The build reads every usage of callable with
this declaration.
| Argument | Type | Description |
|---|---|---|
| callable | Function or class | What renders the field. Must be exported from the same file. |
| declaration | Object | See the options below. |
Returns: a declaration, passed to resolveEditable() or bindActions().
- React
- Plain JS
import { defineEditableSource, prop, resolveEditable } from "@livepreso/webpack-deck";
import { useTargetKey } from "@livepreso/content-react";
export function Headline(props) {
const { attrs } = resolveEditable(editable, props, { targetKey: useTargetKey() });
return <h1 {...attrs}>{props.children}</h1>;
}
const editable = defineEditableSource(Headline, {
everyone: { key: "{targetKey}-{id}-{instance}" },
personal: { when: prop("hasPersonalChannel"), key: "{targetKey}-{id}-{instance}" },
});
<Headline id="title" hasPersonalChannel>
Plans that grow with you
</Headline>
import { defineEditableSource, prop, resolveEditable } from "@livepreso/webpack-deck";
export function headline(options, targetKey) {
const { attrs } = resolveEditable(editable, options, { targetKey });
return `<h1 ${toAttributeString(attrs)}>${options.text}</h1>`;
}
const editable = defineEditableSource(headline, {
everyone: { key: "{targetKey}-{id}-{instance}" },
personal: { when: prop("hasPersonalChannel"), key: "{targetKey}-{id}-{instance}" },
});
headline({ id: "title", hasPersonalChannel: true, text: "Plans that grow with you" }, "plans");
on the slide "plans"
key (everyone) plans-title-{instance}
key (personal) plans-title-{instance}
{instance} stays in the key exactly like this until whatever hosts the deck fills it - see key.
| Option | Type | Default | Description |
|---|---|---|---|
| everyone | Object | Stores the field for every preso. | |
| personal | Object | Stores the field for one preso. | |
| fields | Object or Array | one field, id from id | Which fields this produces. |
| type | String or prop() | "text" | What kind of field. |
| toolbar | Array or prop() | prop("toolbar") | What formatting a text field permits. |
| props | Object | Values handed to the actions as props. | |
| actions | Object | a plain get and set | The queries and mutations, from defineActions(). |
At least one of everyone and personal is required.
everyone and personal
See Channels for what each one means.
| Option | Type | Description |
|---|---|---|
| when | prop() | Uses the channel only where this argument is truthy. Omitted, the channel is always used. |
| key | String | Required. The key template. |
| keyWhen | Object | A different key template, chosen by an argument. |
when
Some headlines may be tailored in Prep, and some may not.
personal: { when: prop("hasPersonalChannel"), key: "{id}" },
<Headline id="title" hasPersonalChannel /> Prep can tailor it
<Headline id="title" /> Prep cannot
key
| Placeholder | Filled with |
|---|---|
{targetKey} | The key of the slide, section or template, as project.yaml spells it. |
{id} | The field's id. |
{instance} | Which copy of the target this is. See {instance} below. |
<Disclaimer id="fees" /> on the slide "pricing"
key: "{targetKey}-{id}-{instance}" -> pricing-fees-{instance} one per copy
key: "{targetKey}-{id}" -> pricing-fees one per slide
key: "{id}" -> fees one per id
key: "legal" -> legal one per deck
The first three have a name, so a declaration can import one instead of retyping the template:
import { DECK_WIDE, ONE_PER_COPY, ONE_PER_TARGET } from "@livepreso/webpack-deck";
everyone: { key: ONE_PER_COPY }, // "{targetKey}-{id}-{instance}"
everyone: { key: ONE_PER_TARGET }, // "{targetKey}-{id}"
everyone: { key: DECK_WIDE }, // "{id}"
On everyone that loses content: every value PresoManager stored under the old key goes, for every preso made from the
deck. On personal it loses what a salesperson typed into presos already made, and presos made afterwards start from
the source copy as they always would.
There is no default key for the same reason: a key the build chose could not be changed afterwards either. Every channel names one. :::
{instance}
A template can be placed more than once, and a custom slide can be copied from one. {targetKey}-{id} alone is the same
key for every copy, so they would all show one value. Add {instance} to keep each copy's value separate.
The build leaves {instance} in the key exactly as written. Whatever hosts the deck fills it when it stores or reads
the value, never the build. It is empty for a slide the deck itself declares, and for a user template sitting in the
library, so the dash written in front of it drops too.
<Disclaimer id="fees" /> declared with key: "{targetKey}-{id}-{instance}"
on the deck's own "pricing" slide pricing-fees
on one placement of a template pricing-fees-autoadjunct-7
on another placement of the same one pricing-fees-autoadjunct-24
scope
A channel's scope says who shares its value:
"instance", the default, keeps the key as written: one value per placement of the slide."deck"leaves{targetKey}and{instance}out of the key: one value shared by every slide and placement using the id.
Most disclaimers belong to their slide. One that repeats on every slide should be edited once.
everyone: {
key: "{targetKey}-{id}-{instance}",
scope: prop("scope", { default: "instance" }),
},
on the slide "pricing"
<Disclaimer id="fees" /> pricing-fees-{instance}
<Disclaimer id="fees" scope="deck" /> fees
A scope other than "instance" or "deck" is refused.
keyWhen
keyWhen picks another key template when an argument is truthy. Where several are, the first one written wins.
everyone: {
key: "{targetKey}-{id}-{instance}",
keyWhen: { isSummary: "{targetKey}-summary-{id}-{instance}" },
},
fields
// One field. Its id is the usage's `id` argument. The default.
fields: { id: prop("id") },
// One field, read from another argument.
fields: { id: prop("idFromUsage") },
// Several fields from one component, each written out.
fields: [{ id: "plan-name" }, { id: "plan-photo", type: "image" }],
| Entry key | Type | Description |
|---|---|---|
| id | String or prop() | Required. |
| description | String or prop() | What the field is for, told to whoever edits it. A single field defaults to prop("description"). |
| defaultValue | String or prop() | The copy the field shows before anything is stored, where the usage writes it as an argument rather than between its tags. |
| type | String | Overrides the declaration's type. |
| actions | Object | Overrides the declaration's actions. List form only. |
Where the starting value comes from
JSX children are the one shape read without being declared. Write the value between the tags and nothing else is needed:
<Headline id="intro">The copy it starts with</Headline>
Every other shape needs defaultValue, naming the argument the value arrives in. That covers a tag passing it as an
argument, and a plain call, which has no children at all:
fields: { id: prop("id"), defaultValue: prop("heading") },
<Headline id="intro" heading="The copy it starts with" />;
headline({ id: "intro", heading: "The copy it starts with" });
Without it, the field reads as empty until somebody stores something. Children win where a usage writes both.
With several fields, the key must be built from {id}, or every entry would be stored under one key.
Picking one of several
A list registers every entry. Which one a rendering draws is resolveEditable's field option, not the arguments it
was called with:
resolveEditable(editable, props, { targetKey: useTargetKey(), field: "plan-photo" });
Leave field out and every rendering draws the first entry in the list. No warning says so.
One field per row
A slide rendering a row at a time keys each one off the row. The build cannot read the loop, so the ids come from the list, and the list is content your deck already holds:
export const PLANS = [
{ id: "starter", name: "Starter" },
{ id: "growth", name: "Growth" },
{ id: "scale", name: "Scale" },
];
import { PLANS } from "./plans.js";
export function Headline({ id, children }) {
const { attrs } = resolveEditable(
editable,
{},
{
targetKey: useTargetKey(),
field: id,
},
);
return <h3 {...attrs}>{children}</h3>;
}
const editable = defineEditableSource(Headline, {
fields: PLANS,
everyone: { key: "{targetKey}-{id}" },
});
export function Pricing() {
return PLANS.map((plan) => <Headline id={plan.id}>{plan.name}</Headline>);
}
One list, read twice. The build follows the import and registers pricing-starter, pricing-growth and
pricing-scale; each rendering hands its own id to field. An entry may carry anything else your content needs -
name here - and the build reads only id.
Keys built from a feed are the shape this cannot cover. The rows arrive with the preso, so no list names them, and the field is not registered. See id-not-written-out.
Ids from a declared function
A function can return the ids instead of rendering anything. Declare it with a list, and an id picked off what it returned is one of that list, whichever property is picked:
export function sectionIds({ heading = "heading", body = "body" } = {}) {
return { heading, body };
}
defineEditableSource(sectionIds, {
fields: [{ id: prop("heading") }, { id: prop("body") }],
everyone: { key: "{targetKey}-{id}" },
});
const options = { heading: "title" };
export function Section({ part }) {
const ids = sectionIds(options);
return (
<>
<Headline id={ids.heading} />
<Body id={ids[part]} />
</>
);
}
The call has to be in the same file as the usage, assigned to a const. On the slide "pricing" this registers
pricing-title and pricing-body.
A component shared by several slides can make the same call with options it was handed. The build cannot read those options, so that call adds no fields, and the slide's own call lists them:
export function SectionBody({ options, part }) {
const ids = sectionIds(options);
return <Body id={ids[part]} />;
}
Where no call on a slide can be read, the build warns with arguments-spread.
type
| Type | Channels | Description |
|---|---|---|
text | both | Text. Formatting is set by toolbar. |
richtext | both | The same as text in markup. |
image | both | An image URL. |
video | everyone | A video URL. |
type: "image",
type: prop("kind", { default: "text" }),
toolbar
What formatting a text field permits. Most declarations leave it out:
| The component | toolbar |
|---|---|
takes a toolbar prop | Leave it out. The default, prop("toolbar"), reads the prop. |
| draws plain text | Leave it out. No toolbar permits plain text only. |
| decides its own formatting, with no toolbar prop | Write it out, as below. |
| has actions | Leave it out. It is never read: see With actions. |
toolbar: ["style", "link"],
| Option | Permits |
|---|---|
style | Bold and italic |
color | Text colour, from the deck's palette |
superscript | Superscript and subscript |
link | Links |
align | Alignment |
removeformat | Nothing. A button that strips formatting. |
list | Lists. Needs a div. |
table | Tables. Needs a div. |
format | Headings and other block formats |
With actions
toolbar describes the app supplied text field, which stores through data-toolbar. A declaration with
actions stores through those instead, and what they accept is decided by the input each one describes - so
toolbar is never consulted, and the options above do not apply to it.
defineEditableSource(Headline, {
toolbar: ["style", "colour"], // colour is refused: not one of the options above
everyone: { key: "{targetKey}-{id}" },
});
defineEditableSource(Headline, {
actions: headline,
toolbar: ["style", "colour"], // not read at all
everyone: { key: "{targetKey}-{id}" },
});
A field with actions is free to take a toolbar of its own through props, in whatever vocabulary the
component it renders expects.
props
props: { currency: prop("currency") },
<Price id="monthly" currency="AUD" /> -> props: { currency: "AUD" }
The actions read these as props.
actions
actions: price,
price is what defineActions() returned.
Actions in the same file
actions can be written inline, or as a defineActions() call in the same file as what renders
the field. Either way, a query or mutation can use that file's imports and top-level consts and functions - everything
except what renders the field itself. Anything declared inside a query or mutation's own function is always fine.
const TAX = 0.1;
actions: {
queries: {
total: ({ state }) => state.amount * (1 + TAX);
}
} // works
let TAX = 0.1;
// fails the build: TAX is declared with let
actions: {
queries: {
total: ({ state }) => state.amount * (1 + TAX);
}
}
actions: {
queries: {
total: ({ state }) => {
let sum = state.amount; // declared inside the function - always fine
return sum;
},
},
},
A top-level let or var fails the build. Declare it with const instead, or move it inside the query or mutation
that reads it.
prop()
prop(name, [{ default }])
Reads a value from the usage's argument name.
type: "image" always image
type: prop("kind") whatever kind= says
type: prop("kind", { default: "image" }) the same, and image where kind= is left out
A default in the callable's parameters is read too, from the parameter the arguments arrive in (see A plain call):
// Already read. No default needed in prop().
export function Price({ currency = "AUD" }) {}
// The body decides the fallback, so write it in prop("currency", { default: "AUD" }).
export function Price({ currency }) {
const shown = currency || "AUD";
}
Two different defaults for one argument are refused.