Skip to main content

Best practices

This page covers best practices on how to design pleasant-to-use blocks.

Allow saving a block with its default values

Adding a new block to a document should not prevent a document's saving – even if the block is incomplete. Consider the following example:

headline.block.ts
class HeadlineBlockInput extends BlockData {
@BlockField()
@IsString()
@IsNotEmpty()
eyebrow: string;

@ChildBlockInput(RichTextBlock)
@ValidateNested()
headline: ExtractBlockInput<typeof RichTextBlock>;
}

This headline block has a required eyebrow field. Consequently, the block can only be saved after the user enters a text in the eyebrow field. If they forget to add an eyebrow text in a headline block deep down in the page content block, saving is impossible. They must navigate to the respective block, enter some text, and then save. Furthermore, they may not even want to have an eyebrow for the specific headline block.

To improve the user experience, make sure that the user can save the block without having to enter an eyebrow text:

headline.block.ts
class HeadlineBlockInput extends BlockData {
@BlockField({ nullable: true })
@IsString()
@IsOptional()
eyebrow?: string;

...
}

Make sure to consider all three aspects of the field: First, add nullable: true to the @BlockField decorator for the block meta (and, therefore, the generated TypeScript interfaces). Second, add a @IsOptional decorator for the validation of the block input. Finally, denote the property as optional using ? for TypeScript.

note

This best practice doesn't mean you should never validate blocks. For instance, you should validate an email for correctness. However, you should only validate if the user has entered some text.

Expect incomplete block data

As we have seen above, blocks should never prevent users from saving. Therefore, the clients of our API may receive incomplete block data. For instance, in the above-mentioned headline block example, the eyebrow text may still be missing. The client should handle the missing eyebrow text accordingly.

HeadlineBlock.tsx
export const HeadlineBlock = withPreview(
({ data: { eyebrow, headline } }: PropsWithData<HeadlineBlockData>) => {
return (
<>
{eyebrow && <small>{eyebrow}</small>}
<RichTextBlock data={headline} />
</>
);
},
{ label: "Headline" },
);

Note how we only render the eyebrow if the user has entered a text.

React to empty states over using toggles/switches

Parts of a block can be optional in some instances. For instance, a teaser block may have an optional call to action button. One may be tempted to use the OptionalBlock factory to achieve this behavior. However, using the factory introduces additional complexity for users by requiring them to toggle the visibility. A better solution is only displaying the button if it is maintained completely.

TeaserBlock.tsx
export const TeaserBlock = withPreview(
({ data: { image, callToAction } }: PropsWithData<TeaserBlockData>) => {
return (
<>
<ImageBlock data={image} />
{callToAction.text.length > 0 && <button>{callToAction.text}</button>}
</>
);
},
{ label: "Teaser" },
);