Skip to content

Composing components

A frame or a button background wraps other markup. Declare a children prop and place {children} in your markup:

src/chrome.ts2
export const SunkenBox = ({ children }: { children?: Children }): Component => (
<layer w="fill" h="fill">
<rect w="fill" h="fill" color={Colors.WELL} fill transparency={80} />
<rect w="fill" h="fill" color={Colors.BLACK} />
<rect x="center" y="center" w={{ fill: 2 }} h={{ fill: 2 }} color={0x474745} />
<layer w="fill" h="fill">{children}</layer>
</layer>
);

Anything written between the tags is built into that inner layer:

src/quest-log.ts2
<SunkenBox>
…
<layer ref={list} x={2} y={2} w={{ fill: 22 }} h={{ fill: 4 }}
scrollHeight={QUESTS.length * ROW_H}>

{children} appears once, inside an element.

A wrapper can hand its children to another component instead of having a slot of its own:

src/chrome.ts2
export const StoneButton = ({ children }: { children?: Children }): Component => (
<NineSlice
topLeft={StoneSprites.TOP_LEFT} topRight={StoneSprites.TOP_RIGHT}
bottomLeft={StoneSprites.BOTTOM_LEFT} bottomRight={StoneSprites.BOTTOM_RIGHT}
left={StoneSprites.LEFT} top={StoneSprites.TOP}
right={StoneSprites.RIGHT} bottom={StoneSprites.BOTTOM}
>
{children}
</NineSlice>
);

StoneButton and StoneButtonPressed are two skins over one NineSlice, whose slot is where the content ends up.

A prop the wrapper does not declare falls through to the first element underneath. ClaimButton declares only its label:

src/layout.ts2
const ClaimButton = ({ label }: { label: String }): Component => (
<StoneButton>
<text w="fill" h="fill" text={label} font={Fonts.BOLD}
align="center" valign="middle" shadow color={Colors.AMBER} />
</StoneButton>
);
src/layout.ts2
<ClaimButton label="Claim" right={0} bottom={0} w={74} h={22} options={['Claim']} />

The position, size and options land on NineSlice’s <layer>. See Passing props.

When a prop would switch a component’s appearance, write two components and choose between them:

src/shop.ts2
{tab === i
? (
<StoneButtonPressed>
<text w="fill" h="fill" text={label} font={Fonts.BOLD}
align="center" valign="middle" shadow color={Colors.WHITE} />
</StoneButtonPressed>
)
: (
<StoneButton>
<text w="fill" h="fill" text={label} font={Fonts.BOLD}
align="center" valign="middle" shadow color={Colors.AMBER} />
</StoneButton>
)}

Markup is a value, so you can bind a piece of it to a const and splice it in by name:

export const Panel = ({ warn }: { warn: Boolean }): Component => {
const banner = (
<layer w="fill" h={20}>
<rect w="fill" h="fill" color={0x8b0000} fill />
</layer>
);
return <layer w="fill" h="fill">{warn && banner}<Contents /></layer>;
};

A name is not a tag: {banner} works, <banner /> does not. Declare a component when you want a tag or props.

Reusable components go in their own file, exported by name:

src/shop.ts2
import { StoneButton, StoneButtonPressed, SunkenBox, Window } from './chrome';

Take the art as props, so one frame serves every panel:

src/chrome.ts2
const NineSlice = ({
topLeft, topRight, bottomLeft, bottomRight, left, top, right, bottom, children,
}: FrameSprites & { children?: Children }): Component => (
<layer w="fill" h="fill">
<sprite x="left" y="top" w={CORNER} h={CORNER} sprite={topLeft} tiling />
{/* … the other seven … */}
<layer w="fill" h="fill">{children}</layer>
</layer>
);

To act on something the caller owns, take its Ref as a prop and write to it, as the scrollbar does to the list it scrolls:

src/chrome.ts2
export const Scrollbar = ({ content }: { content: Ref }): Component => {
// … and it binds the wheel on the thing it was given:
content.onScrollWheel = (event) => {
content.scrollY = content.scrollY + event.mouseY * WHEEL;
trackScroll(gutter, thumb, capTop, capBottom, content);
};
// …
};

Refs has the rest.