Skip to main content

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.

ArgumentTypeDescription
callableFunction or classWhat renders the field. Must be exported from the same file.
declarationObjectSee the options below.

Returns: a declaration, passed to resolveEditable() or bindActions().

headline.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}" },
});
usage
<Headline id="title" hasPersonalChannel>
Plans that grow with you
</Headline>
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.

OptionTypeDefaultDescription
everyoneObjectStores the field for every preso.
personalObjectStores the field for one preso.
fieldsObject or Arrayone field, id from idWhich fields this produces.
typeString or prop()"text"What kind of field.
toolbarArray or prop()prop("toolbar")What formatting a text field permits.
propsObjectValues handed to the actions as props.
actionsObjecta plain get and setThe queries and mutations, from defineActions().

At least one of everyone and personal is required.

everyone and personal​

See Channels for what each one means.

OptionTypeDescription
whenprop()Uses the channel only where this argument is truthy. Omitted, the channel is always used.
keyStringRequired. The key template.
keyWhenObjectA 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​

PlaceholderFilled 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}"
A key cannot change once a deck is in use.

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 keyTypeDescription
idString or prop()Required.
descriptionString or prop()What the field is for, told to whoever edits it. A single field defaults to prop("description").
defaultValueString or prop()The copy the field shows before anything is stored, where the usage writes it as an argument rather than between its tags.
typeStringOverrides the declaration's type.
actionsObjectOverrides 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:

plans.js
export const PLANS = [
{ id: "starter", name: "Starter" },
{ id: "growth", name: "Growth" },
{ id: "scale", name: "Scale" },
];
Pricing.js
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:

section-ids.js
export function sectionIds({ heading = "heading", body = "body" } = {}) {
return { heading, body };
}

defineEditableSource(sectionIds, {
fields: [{ id: prop("heading") }, { id: prop("body") }],
everyone: { key: "{targetKey}-{id}" },
});
Section.js
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:

Body.js
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​

TypeChannelsDescription
textbothText. Formatting is set by toolbar.
richtextbothThe same as text in markup.
imagebothAn image URL.
videoeveryoneA video URL.
type: "image",
type: prop("kind", { default: "text" }),

toolbar​

What formatting a text field permits. Most declarations leave it out:

The componenttoolbar
takes a toolbar propLeave it out. The default, prop("toolbar"), reads the prop.
draws plain textLeave it out. No toolbar permits plain text only.
decides its own formatting, with no toolbar propWrite it out, as below.
has actionsLeave it out. It is never read: see With actions.
toolbar: ["style", "link"],
OptionPermits
styleBold and italic
colorText colour, from the deck's palette
superscriptSuperscript and subscript
linkLinks
alignAlignment
removeformatNothing. A button that strips formatting.
listLists. Needs a div.
tableTables. Needs a div.
formatHeadings 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.