Fields: f.*
On this page
fields is the list of settings a seller can change on a block. You write it once, and from it
BizzPocket gets everything else:
- the props of
render(typed for you, no extra type to write), - the default of each prop,
- the controls in the page editor (text box, image picker, collection picker...),
- the checks that make a saved value safe before
rendersees it.
import { block, f, Image, Link } from '@bizzpocket/theme';
export default block({
name: 'Promo',
category: 'promo',
fields: {
headline: f.text({ default: 'Summer sale' }),
image: f.image(),
ctaLabel: f.text({ default: 'Shop now' }),
ctaHref: f.link({ default: '/shop' }),
},
render: ({ headline, image, ctaLabel, ctaHref }) => (
<section>
<h2>{headline}</h2>
{image ? <Image src={image.src} alt={image.alt ?? ''} width={1200} height={600} sizes="100vw" /> : null}
{ctaHref ? <Link href={ctaHref}>{ctaLabel}</Link> : null}
</section>
),
});A link is always f.link() (it only ever holds a safe URL) rendered with <Link>, and a picture
f.image() with <Image>. A plain f.text in an href is whatever was saved: another site
(//elsewhere.example), a data: URL, anything.
Contents: All field kinds · Options every field has · Defaults · Labels, tiers and hidden fields · Lists and groups · Blocks inside a block · Traits · What happens to a bad saved value · Types
All the field kinds
There is one kind for each job. There are no synonyms.
| Field | render gets |
Use it for | Editor control |
|---|---|---|---|
f.text(opts?) |
string |
a short piece of text | text box (a multi-line box with multiline: true; a link picker when the key ends in href) |
f.richText(opts?) |
string (safe HTML) |
formatted text: bold, links, lists | rich-text editor |
f.number(opts?) |
number |
a count, a size, a delay | number box |
f.boolean(opts?) |
boolean |
an on/off switch | toggle |
f.select(options, opts?) |
one of your option values | choose one from a fixed list | dropdown |
f.color(opts?) |
string (a colour) |
a colour | colour picker |
f.image(opts?) |
{ src, alt? } | undefined |
a picture | image picker |
f.video(opts?) |
{ src, poster? } | undefined |
a video | video picker |
f.video({ format: 'url' }) |
string | undefined (the video URL) |
a video saved as a plain URL (2.x videoUrl-style content; migrate writes this) |
video picker |
f.link(opts?) |
string | undefined |
where a button or link goes | link picker |
f.product(opts?) |
string | undefined (a product handle) |
choose a product | product picker |
f.collection(opts?) |
string | undefined (a collection slug) |
choose a collection | collection picker |
f.category(opts?) |
string | undefined |
choose a category | category picker |
f.menu(opts?) |
string | undefined (a menu handle) |
choose a navigation menu | menu picker |
f.list(itemFields, opts?) |
Array<item> |
a repeating group, such as slides or FAQs | repeater |
f.group(fields, opts?) |
an object | a few fields kept together | grouped controls |
f.json<T>(opts?) |
T |
anything else (an escape hatch; use sparingly) | JSON box |
f.blocks(opts?) |
ReactNode[] (the child blocks, already rendered) |
a container: blocks the seller puts inside this one | "add block inside" (group.md) |
f.spacing(), f.colorScheme(), f.typography() |
not a prop | see Traits | spacing / colour / type controls |
A shape the builder cannot describe is a build error, never a silent unknown.
Field names. Do not give a field the name of a data helper (product, products, menu,
store, categories, home, search, related, gql). Destructured in data(), it hides the
helper: ({ product }) => ({ item: product({ handle: product }) }) calls the seller's string. Use
productHandle, menuHandle and so on (data.md, rule 3).
Examples of each
import { block, f } from '@bizzpocket/theme';
export default block({
name: 'Field tour',
category: 'content',
fields: {
// Text
title: f.text({ default: 'Hello', maxLength: 60, placeholder: 'A short title' }),
intro: f.text({ multiline: true, help: 'One or two sentences.' }),
note: f.text({ optional: true }), // string | undefined, no default
body: f.richText(),
// Numbers and switches
columns: f.number({ default: 3, min: 1, max: 6, step: 1 }),
showPrices: f.boolean({ default: true }),
// One of a list. A plain string is both the value and the label.
layout: f.select(['grid', { value: 'list', label: 'List view' }], { default: 'grid' }),
// A colour: #rgb, #rrggbb, rgb(), hsl() or var(--token)
accent: f.color({ default: '#111111' }),
// Media and links
banner: f.image(),
clip: f.video(),
buttonLink: f.link(),
// Store resources: each holds a handle or slug the data helpers understand
featured: f.product(),
collection: f.collection(),
category: f.category(),
menu: f.menu(),
// Repeating and grouped fields
slides: f.list({ heading: f.text(), picture: f.image() }, { max: 6, label: 'Slides' }),
cta: f.group({ label: f.text({ default: 'Shop now' }), href: f.link() }, { label: 'Button' }),
// Anything else
config: f.json<{ speed: number }>({ default: { speed: 3 } }),
},
render: (p) => (
<section style={{ color: p.accent }} data-columns={p.columns}>
<h2>{p.title}</h2>
<p>{p.intro}</p>
{p.note ? <small>{p.note}</small> : null}
<div dangerouslySetInnerHTML={{ __html: p.body }} />
{p.banner ? <img src={p.banner.src} alt={p.banner.alt ?? ''} /> : null}
<ul data-layout={p.layout}>
{p.slides.map((s, i) => (
<li key={i}>{s.heading}</li>
))}
</ul>
<a href={p.cta.href ?? '#'}>{p.cta.label}</a>
<span hidden>{String(p.showPrices)}{p.config.speed}</span>
</section>
),
});Options every field has
These work on every kind except list, spacing, colorScheme and typography (see below).
| Option | What it does |
|---|---|
label |
The name shown in the editor. If you leave it out it is made from the key: subLabel becomes "Sub label". |
help |
A line of help text under the control. |
group |
A heading to group fields under in the editor, such as 'Button'. |
tier |
'basic' or 'advanced'. See Labels, tiers and hidden fields. |
hidden |
true keeps the field out of the editor. It still has a value and still reaches render. |
default |
The value used until the seller changes it. |
optional |
true means "may be empty". The prop's type gains ` |
Kind-specific options:
| Field | Extra options |
|---|---|
f.text |
multiline (a larger box), maxLength (longer text is cut), placeholder |
f.number |
min, max (values outside are pulled in), step, integer (whole numbers: a fraction is truncated, the editor asks for a whole number) |
f.select(options, opts) |
options is an array of strings or { value, label } objects |
f.list(item, opts) |
label, help, group, tier, min, max (no default; a list starts empty) |
Defaults
If a field has no default option, its prop starts as:
| Kind | Starts as |
|---|---|
text, richText, color |
'' (empty text) |
number |
min if you set one, else 0 |
boolean |
false |
select |
the first option's value |
list |
[] |
group |
the defaults of its fields |
image, video, link, product, collection, category, menu, json |
undefined |
any field with optional: true |
undefined |
blocks |
no default: its children come from the layout ([] when there are none) |
The default is used when a seller never set the value, when the saved value is empty, or when it was
invalid. It is also what data() sees (see data.md). The defaults are saved in
dist/blocks.json as the block's defaultProps.
Because f.link(), f.collection(), f.image() and the other resource kinds can be undefined,
check before use ({image ? <Image ... /> : null}), or give them a default.
Labels, tiers and hidden fields
You rarely need to set these. The defaults are chosen so most blocks need no extra options.
Labels come from the key. headline is "Headline", ctaHref is "Cta href" (set label: 'Button link'
if you want better).
Tiers decide what the editor shows first.
basic: the everyday content:text,richText,image,videoandlinkfields (and text fields that become link pickers).advanced: everything else (numbers, switches, selects, colours, resource pickers, lists, groups, json, traits).
Override with tier: 'basic' or tier: 'advanced' on any field.
Hidden fields are not shown in the editor. A text field is hidden by itself when its key looks like
plumbing: id, slug, key, or a key ending in Id, Slug, Key, _id, -slug and so on (for
example productId). Set hidden: true to hide any other field.
Link controls. A text field whose key ends in href or Href uses the link picker, just like
f.link().
Lists and groups
f.list is a repeating row. Give it the fields of one item:
slides: f.list(
{ heading: f.text({ default: 'Slide' }), picture: f.image(), link: f.link() },
{ max: 6, label: 'Slides' },
),In render, slides is an array of { heading, picture, link } (each item is read with the same
rules as any fields). Items beyond max are dropped. The array is never undefined. If the seller
added none, it is [], so render can write slides.map(...) directly.
f.group keeps related fields together under one key:
button: f.group({ label: f.text({ default: 'Shop now' }), href: f.link() }),
// render: button.label, button.hrefBoth can be nested.
Blocks inside a block: f.blocks()
f.blocks() makes a block a container: the seller adds other blocks inside it, and render gets
them, already rendered, as an array to place:
fields: {
title: f.text(),
items: f.blocks({ allow: ['hero', 'category:media'], max: 4 }),
},
render: ({ title, items }) => (
<div className="panel">
<h2>{title}</h2>
<div className="panel__items">{items}</div>
</div>
),It is not like the other fields:
- the children are saved in the layout as the node's
children, not inprops; the field has no default and is not indefaultProps; allow(block types,'category:<name>', or'*') andmax(default 24) are the editor's rules, written toblocks.jsonasx-bp-children, and the renderer enforces them;- one per block, at the top level of
fields(not insidef.listorf.group).
The whole story, with a layout JSON and the registry's ready-made group block, is in
group.md.
Traits: spacing, colours and type
Some settings are the same on many blocks: the space around, the background colour, the font. A trait is a ready-made group of such settings. It does not become a prop. Instead the page puts the chosen values on the block's wrapper as CSS variables, and you use them in your CSS.
fields: {
title: f.text(),
spacing: f.spacing(), // --bp-spacing-top / -right / -bottom / -left
scheme: f.colorScheme(), // --bp-color-background / --bp-color-text
type: f.typography(), // --bp-type-font / -size / -weight
},.promo {
padding-top: var(--bp-spacing-top);
padding-bottom: var(--bp-spacing-bottom);
background: var(--bp-color-background);
color: var(--bp-color-text);
font-family: var(--bp-type-font);
}What the seller picks and what the variable becomes:
| Trait | Properties | Variable value |
|---|---|---|
f.spacing() |
top, right, bottom, left |
a spacing token name, such as l, becomes var(--spacing-l) |
f.colorScheme() |
background, text |
a colour token name, such as primary, becomes var(--colors-primary) |
f.typography() |
font, size, weight |
font is a font token (var(--fonts-sans)); size and weight are used as written, such as 1.25rem and 600 |
Rules:
- A block can have one trait of each kind. Two
f.spacing()fields in one block is an error. - Traits take only
label:f.spacing({ label: 'Padding' }). - A trait that the seller left empty writes no variable, so give your CSS a fallback:
padding-top: var(--bp-spacing-top, 2rem). - The values come from your theme's tokens (theme-config.md).
What happens to a bad saved value
Saved values come from sellers, importers and old versions of your block, so they can be wrong. Before
render runs, every value is cleaned using its field. You never need to re-check them in render.
| Kind | Cleaning |
|---|---|
text |
trimmed; cut to maxLength; a non-string falls back to the default |
richText |
cleaned to a safe set of tags (scripts and event handlers are removed) |
number |
'9' becomes 9; pulled into min..max; not a number gives the default |
boolean |
only real true/false; anything else gives the default |
select |
must be one of the options, else the default |
color |
must be a valid colour (#abc, #aabbcc, rgb(), hsl(), var(--x)), else the default |
image, video |
needs a src that is https: or a path starting with a single /, else undefined |
link |
a path, #anchor, https:, mailto: or tel: link; javascript: and //host become undefined |
product, collection, category, menu |
a non-empty string, else the default |
list, group |
each item or member is cleaned with its own fields; unknown keys are dropped |
json |
passed through as is (clean it yourself) |
blocks |
a saved value under its key is ignored; render gets the rendered children (or []) |
Unknown keys in the saved data are dropped.
Types
You do not write a props type. render's props are worked out from fields (and from data()), so
an editor can complete and check them:
fields: { count: f.number({ default: 4 }), note: f.text({ optional: true }) },
render: ({ count, note }) => {
count; // number
note; // string | undefined
}To name the type elsewhere (for example in a component) use PropsOf:
import { f, type PropsOf } from '@bizzpocket/theme';
const fields = { title: f.text(), count: f.number() };
type Props = PropsOf<typeof fields>; // { title: string; count: number }The editor reads dist/blocks.json. For each field it holds a standard JSON Schema with these extra
keys: x-bp-control (which control), x-bp-label, x-bp-help, x-bp-group, x-bp-tier and
x-bp-hidden, and default. A container's schema also has x-bp-children
({ childrenField, allow, max }), and the block is "container": true.
Next: data.md.