canvas-component-composability

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese
Prefer small, focused components over monolithic ones with many props. When a component starts accumulating many unrelated props, it's often a sign that it should be decomposed into smaller, composable pieces.
优先选择小巧、功能单一的组件,而非拥有大量props的单体组件。 当一个组件开始积累大量不相关的props时,通常意味着它应该被分解为更小、可组合的组件片段。

Signs a component should be decomposed

组件需要分解的信号

Consider breaking up a component when it has:
  • More than 6-8 props that serve distinct purposes
  • Props for elements that make sense as standalone components (breadcrumbs, titles, metadata, navigation)
  • Built-in layout assumptions that limit where the component can be used
  • Multiple distinct visual sections that could be reused independently
当组件出现以下情况时,考虑将其拆分:
  • 超过6-8个用途不同的props
  • 包含可作为独立组件的元素props(如面包屑、标题、元数据、导航)
  • 内置布局假设,限制了组件的使用场景
  • 多个可独立复用的不同视觉区域

Use slots for flexible composition

使用插槽实现灵活组合

Slots are the primary mechanism for composability. Instead of passing complex data through props, use slots to let parent components accept child components. This matches how Canvas users build pages—by placing components inside other components.
插槽是实现组件组合性的核心机制。 不要通过props传递复杂数据,而是使用插槽让父组件接收子组件。这与Canvas用户构建页面的方式一致——将组件嵌套在其他组件内部。

Prefer slots over complex props

优先使用插槽而非复杂props

When a component needs to render variable content, use a slot instead of props with complex structures:
jsx
// Wrong
const ResourceDetail = ({
  metadata: [
    { label: "Type", value: "Report" },
    { label: "Author", value: "UNICEF" },
  ],
}) => (
  <div>
    {metadata.map((item) => (
      <MetadataItem key={item.label} {...item} />
    ))}
  </div>
);

// Correct
const ResourceMetadata = ({ items }) => (
  <div className="flex flex-col gap-2">{items}</div>
);

// Usage: pass MetadataItem components through the slot
<ResourceMetadata
  items={
    <>
      <MetadataItem label="Type" value="Report" />
      <MetadataItem label="Author" value="UNICEF" />
    </>
  }
/>;
当组件需要渲染可变内容时,使用插槽替代结构复杂的props:
jsx
// 错误示例
const ResourceDetail = ({
  metadata: [
    { label: "Type", value: "Report" },
    { label: "Author", value: "UNICEF" },
  ],
}) => (
  <div>
    {metadata.map((item) => (
      <MetadataItem key={item.label} {...item} />
    ))}
  </div>
);

// 正确示例
const ResourceMetadata = ({ items }) => (
  <div className="flex flex-col gap-2">{items}</div>
);

// 使用方式:通过插槽传递MetadataItem组件
<ResourceMetadata
  items={
    <>
      <MetadataItem label="Type" value="Report" />
      <MetadataItem label="Author" value="UNICEF" />
    </>
  }
/>;

Slots enable Canvas compatibility

插槽确保Canvas兼容性

In Drupal Canvas, users compose pages by dragging components into slots. When you design components with slots:
  • Users can add, remove, or reorder child components freely
  • Each child component's props can be edited independently
  • The parent component doesn't need to know about child component types
在Drupal Canvas中,用户通过将组件拖入插槽来组合页面。当你使用插槽设计组件时:
  • 用户可以自由添加、移除或重新排序子组件
  • 每个子组件的props可独立编辑
  • 父组件无需了解子组件的类型

When to use slots vs props

何时使用插槽vs props

Use slots forUse props for
Variable number of child componentsSingle, required values (text, URL)
Content that users should composeConfiguration options (size, color)
Complex nested structuresSimple data (strings, booleans)
Content that varies between instancesContent consistent across instances
适合使用插槽的场景适合使用props的场景
子组件数量可变单一必填值(文本、URL)
用户需要自行组合的内容配置选项(尺寸、颜色)
复杂的嵌套结构简单数据(字符串、布尔值)
不同实例间内容有差异的场景所有实例内容一致的场景

Declare slots in component.yml

在component.yml中声明插槽

Every slot must be declared in the component's
component.yml
:
yaml
slots:
  content:
    title: Content
  sidebar:
    title: Sidebar
In the JSX, slots are received as props and rendered directly:
jsx
const TwoColumnLayout = ({ content, sidebar }) => (
  <div className="grid grid-cols-[1fr_300px] gap-8">
    <div>{content}</div>
    <aside>{sidebar}</aside>
  </div>
);
每个插槽必须在组件的
component.yml
中声明:
yaml
slots:
  content:
    title: Content
  sidebar:
    title: Sidebar
在JSX中,插槽会作为props接收并直接渲染:
jsx
const TwoColumnLayout = ({ content, sidebar }) => (
  <div className="grid grid-cols-[1fr_300px] gap-8">
    <div>{content}</div>
    <aside>{sidebar}</aside>
  </div>
);

Common decomposition patterns

常见的组件分解模式

Page-level elements should be separate components

页面级元素应作为独立组件

Elements that appear on many pages but aren't always needed together should be separate components:
jsx
// Wrong
const ResourceDetail = ({
  breadcrumbItems,
  title,
  date,
  taxonomyTag,
  coverImage,
  downloadButtonUrl,
  metadata,
  description,
}) => (
  <div>
    <Breadcrumb items={breadcrumbItems} />
    <Heading text={title} element="h1" />
    {/* ... */}
  </div>
);

// Correct
const ResourceDetailPage = () => (
  <PageLayout>
    <Section width="wide" content={<Breadcrumb items={breadcrumbItems} />} />
    <Section width="wide" content={<Heading text={title} element="h1" />} />
    <Section width="wide" content={<ArticleMeta date="May 2023" taxonomyTag="Climate" />} />
    <Section width="wide" content={/* ... */} />
  </PageLayout>
);
出现在多个页面但并非总是同时需要的元素,应作为独立组件:
jsx
// 错误示例
const ResourceDetail = ({
  breadcrumbItems,
  title,
  date,
  taxonomyTag,
  coverImage,
  downloadButtonUrl,
  metadata,
  description,
}) => (
  <div>
    <Breadcrumb items={breadcrumbItems} />
    <Heading text={title} element="h1" />
    {/* ... */}
  </div>
);

// 正确示例
const ResourceDetailPage = () => (
  <PageLayout>
    <Section width="wide" content={<Breadcrumb items={breadcrumbItems} />} />
    <Section width="wide" content={<Heading text={title} element="h1" />} />
    <Section width="wide" content={<ArticleMeta date="May 2023" taxonomyTag="Climate" />} />
    <Section width="wide" content={/* ... */} />
  </PageLayout>
);

Extract repeated patterns into small components

将重复模式提取为小型组件

When you see the same combination of elements repeated, extract them:
PatternExtract to component
Date + category/tag
article-meta
Cover image + download button
resource-cover
Label + value pairs
metadata-item
Icon + text link
icon-link
当看到相同的元素组合重复出现时,将其提取为独立组件:
重复模式提取为组件
日期 + 分类/标签
article-meta
封面图 + 下载按钮
resource-cover
标签 + 值对
metadata-item
图标 + 文本链接
icon-link

Use layout components instead of built-in layouts

使用布局组件而非内置布局

Don't build two-column or grid layouts into content components. Use layout components like
grid-container
and compose content into them:
jsx
// Wrong
const ResourceDetail = ({ leftContent, rightContent }) => (
  <div className="flex gap-10">
    <div className="w-[300px]">{leftContent}</div>
    <div className="flex-1">{rightContent}</div>
  </div>
);

// Correct
<GridContainer
  layout="25-75"
  gap="extra_large"
  content={
    <>
      <ResourceCover image={coverImage} />
      <div className="flex flex-col gap-5">
        <ResourceMetadata items={metadata} />
        <Text text={description} />
      </div>
    </>
  }
/>;
不要在内容组件中内置两栏或网格布局,应使用
grid-container
等布局组件,将内容组合到其中:
jsx
// 错误示例
const ResourceDetail = ({ leftContent, rightContent }) => (
  <div className="flex gap-10">
    <div className="w-[300px]">{leftContent}</div>
    <div className="flex-1">{rightContent}</div>
  </div>
);

// 正确示例
<GridContainer
  layout="25-75"
  gap="extra_large"
  content={
    <>
      <ResourceCover image={coverImage} />
      <div className="flex flex-col gap-5">
        <ResourceMetadata items={metadata} />
        <Text text={description} />
      </div>
    </>
  }
/>;

When NOT to decompose

无需分解组件的场景

Keep components together when:
  • They always appear together and never make sense separately
  • They share significant internal state that would be awkward to lift up
  • The visual design tightly couples them (e.g., overlapping elements, shared backgrounds)
  • Decomposition would create components with only 1-2 props that aren't useful elsewhere
在以下情况下,应保持组件的完整性:
  • 组件总是同时出现,单独存在没有意义
  • 组件共享大量内部状态,提升状态会导致代码繁琐
  • 视觉设计紧密耦合(如重叠元素、共享背景)
  • 分解后会生成仅含1-2个props的组件,且无法在其他场景复用