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