Skip to main content

Composing blocks

Sometimes, blocks need different configuration options depending on how they are used. For example, a headline block should have a background in the page content but not inside a column block. In this section, we would like to show how to structure blocks to enable both use cases.

The naive approach: Copy the block

Let's take the example above: One could copy the original headline block and add the additional field:

headline-with-background.block.ts
class HeadlineWithBackgroundBlockData extends BlockData {
@ChildBlock(RichTextBlock)
headline: BlockDataInterface;

@BlockField({ nullable: true })
eyebrow?: string;

+ @BlockField({ type: "enum", enum: BackgroundColor })
+ background: BackgroundColor;
}

class HeadlineWithBackgroundBlockInput extends BlockInput {
@ChildBlockInput(RichTextBlock)
headline: ExtractBlockInput<typeof RichTextBlock>;

@BlockField({ nullable: true })
@IsUndefinable()
@IsString()
eyebrow?: string;

+ @IsEnum(BackgroundColor)
+ @BlockField({ type: "enum", enum: BackgroundColor })
+ background: BackgroundColor;

transformToBlockData(): HeadlineWithBackgroundBlockData {
return inputToData(HeadlineWithBackgroundBlockData, this);
}
}

export const HeadlineWithBackgroundBlock = createBlock(
HeadlineWithBackgroundBlockData,
HeadlineWithBackgroundBlockInput,
"HeadlineWithBackground",
);

While this approach is easy to implement, it has one major disadvantage: the application now has two independent blocks. If a change is to be made in the headline block (e.g., an additional alignment field), it must always be made in both blocks. Furthermore, the code can only be reused to a limited extent.

A better approach: Composing blocks

Instead of copying the original headline block, it can be used to compose a new block:

headline-with-background.block.ts
class HeadlineWithBackgroundBlockData extends BlockData {
@ChildBlock(HeadlineBlock)
headline: BlockDataInterface;

@BlockField({ type: "enum", enum: BackgroundColor })
background: BackgroundColor;
}

class HeadlineWithBackgroundBlockInput extends BlockInput {
@ChildBlockInput(HeadlineBlock)
headline: ExtractBlockInput<typeof HeadlineBlock>;

@IsEnum(BackgroundColor)
@BlockField({ type: "enum", enum: BackgroundColor })
background: BackgroundColor;

transformToBlockData(): HeadlineWithBackgroundBlockData {
return inputToData(HeadlineWithBackgroundBlockData, this);
}
}

export const HeadlineWithBackgroundBlock = createBlock(
HeadlineWithBackgroundBlockData,
HeadlineWithBackgroundBlockInput,
"HeadlineWithBackground",
);

This approach has the advantage that we now have only one headline block, and changes must be made only in that place. Furthermore, the block can be reused in the Admin and the Site, leading to higher code reuse.

info

This approach is based on the principle of composition over inheritance, which is frequently used in Comet.

Why don't create a factory instead?

When considering the example above, the question might arise as to why don't create a block factory instead, where the background option can be optionally activated:

const HeadlineBlock = createHeadlineBlock({
name: "Headline",
});

const HeadlineWithBackgroundBlock = createHeadlineBlock({
name: "HeadlineWithBackground",
background: true,
});

One disadvantage of this approach is that any combination of configuration options (e.g., without background but with alignment) must be supported, even though it is unnecessary. Furthermore, the creation of such a factory is more complex than the relatively simple composition of blocks. A good principle could be: If every option out of N possible options is used precisely once in a block, then creating N composed blocks is better.

note

There are undoubtedly well-founded use cases for block factories, e.g., if a factory is in a library. However, they will rarely be needed in applications.