Content projection

内容投影允许组件将组件的 JSX children视为一种输入形式,并将这些children投射到组件的 JSX 中。

有条件地投射其内容的可折叠组件的示例。

export const Collapsible = component$(() => {
  const store = useStore({ isOpen: true });

  return (
    <div class="collapsible">
      <div class="title" onClick$={() => (store.isOpen = !store.isOpen)}>
        <Slot name="title"></Slot>
      </div>
      {store.isOpen ? <Slot /> : null}
    </div>
  );
});

上面的组件可以像这样从父组件中使用:

export const MyApp = component$(() => {
  return (
    <Collapsible>
      <span q:slot="title">Title text</span>
      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus vulputate accumsan pretium.
    </Collapsible>
  );
});

Collapsible 组件将始终显示title,但只有在 store.isOpentrue 时才会显示文本正文。

渲染输出

如果 isOpen===true,上面的示例将渲染为这个 HTML :

<my-app>
  <collapsible>
    <q:slot q:key="title" has-content>
      <span q:slot="title" has-content>Title text</span>
    </q:slot>
    <q:slot>
      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus vulputate accumsan pretium.
    </q:slot>
  </collapsible>
</my-app>

插槽(Slots)

Qwik 使用插槽作为将内容从父组件投递到子组件的一种方式。

父组件使用 q:slot 属性来标识projection的来源。子组件的 <Slot> 元素来标识projection的目的地。 假定未命名(或未包装)的内容具有 q:slot="" 属性。

export const Project = component$(() => {
  return (
    <div>
      <Slot />
    </div>
  );
});

export const MyApp = component$(() => {
  return (
    <Project>
      unwrapped text
      <span>wrapped text with no q:slot</span>
      <span q:slot="">wrapped text with default name</span>
    </Project>
  );
});

Results in:

<my-app>
  <project>
    <div>
      <q:slot q:key has-content>
        unwrapped text
        <span>wrapped text with no q:slot</span>
        <span q:slot="">wrapped text with default name</span>
      </q:slot>
    </div>
  </project>
</my-app>

具名插槽(Naming slots)

使用 q:slot 属性来命名插槽。

export const Project = component$(() => {
  return (
    <div>
      <div class="title">
        <Slot name="title" />
      </div>
      <Slot />
    </div>
  );
});

export const MyApp = component$(() => {
  return (
    <Project>
      unwrapped text
      <span q:slot="title">first title text</span>
      <span>wrapped text with no q:slot</span>
      <span q:slot="title">second title text</span>
    </Project>
  );
});

渲染结果:

<my-app>
  <project>
    <div>
      <div class="title">
        <span q:slot="title">first title text</span>
        <span q:slot="title">second title text</span>
      </div>
      <q:slot q:key="" has-content>
        unwrapped text
        <span>wrapped text with no q:slot</span>
      </q:slot>
    </div>
  </project>
</my-app>

注意事项:

  • name="" 属性与没有属性或没有包装元素的行为相同。
  • 多个 q:slot="title" 属性在内容投递中会将items合并在一起。

没有插槽时(Not projecting content)

Qwik 保留所有内容,即使没有插槽。这是因为内容可以在未来被投递。

export const Project = component$(() => {
  return <div />;
});

export const MyApp = component$(() => {
  return <Project>unwrapped text</Project>;
});

渲染结果:

<my-app>
  <project>
    <q:template q:slot="">unwrapped text</q:template>
    <div></div>
  </project>
</my-app>

请注意,unwrapped text被移动到惰性 <q:template> 中。 这样做是为了以防 Project 组件重新渲染并插入 <Slot>。 在这种情况下,我们不想重新渲染父组件。两个组件的渲染需要保持独立。

默认插槽内容

如果父组件不提供值,则可以插入默认插槽内容。

export const Project = component$(() => {
  return (
    <>
      <Slot name="title">default title</Slot>
      <Slot>default content</Slot>
    </>
  );
});

export const MyApp = component$(() => {
  return <Project>some content</Project>;
});

渲染结果:

<my-app>
  <project>
    <q:slot q:key="title">
      <q:slot-default>default title</q:slot-default>
    </q:slot>
    <q:slot has-content>
      <q:slot-default>default content</q:slot-default>
      some content
    </q:slot>
  </project>
</my-app>

请注意,默认内容可以写在 <Slot>..default content<Slot> 中。 此内容将始终插入到 结果HTML 的 <q:slot-default> 中。 <q:slot-default> 的可见性由 has-content 属性控制。有关详细信息,请参阅 CSS 部分。

CSS

为了让 Qwik 能够独立渲染组件,它必须能够从 HTML 中读取rules of projection。 这是通过 <q:slot> 元素、<q:slot-default> 元素和 q:slot 属性实现的。 实现这一点还需要额外的元素。为了使元素保持惰性,Qwik 将以下 CSS 添加到 <style> 标记中。

<style>
  q:slot,q:slot-default {
    /** This marks the extra elements inert for flex, etc... **/
    display: contents;
  }
  q:slot.has-content > q:slot-default {
    /** Suppress the default value of Slot if parent provided content **/
    display: none:
  }
</style>

Invalid projection

q:slot 属性必须是组件的直接子元素(子辈元素)。

export const Project = component$(() => { ... })

export const MyApp = component$(() => {
  return (
    <Project>
      <span q:slot="title">ok, direct child of Project</span>
      <div>
        <span q:slot="title">Error, not a direct child of Project</span>
      </div>
    </Project>
  );
});

Projection vs children

所有框架都需要一种方法来让组件以有条件的方式包装其复杂的内容。 这个问题可以通过许多不同的方式解决,但主要有两种方法:

  • projection: Projection is a declarative way of describing how the content gets from the parent template to where it needs to be projected.
  • children: children refers to vDOM approaches that treat content just like another input.

最好将这两种方法描述为声明式与命令式。它们都有各自的优点和缺点。

Qwik 使用声明式projection方法。这样做的原因是 Qwik 需要能够相互独立地渲染父子组件。 使用命令式(children)方法,子组件可以以无数种方式修改children。 如果子组件依赖于 children,则每当父组件重新渲染以将命令式转换重新应用到 children 时,子组件将被迫重新渲染。 额外的渲染明显违背了 Qwik 组件单独渲染的目标。

例如,让我们回到上面的 Collapsible 示例:

  • 父级需要能够更改标题和文本,而无需强制 Collapsible 组件重新渲染。 Qwik 需要能够在不影响 Collapsible 组件的情况下将更改重新分发到 MyApp 模板。
  • 子组件需要更改投影的内容,而无需重新渲染父组件。在我们的例子中,Collapsible 应该能够 隐藏/显示 默认的 q:slot 而无需下载和重新渲染 MyApp 组件。

为了使这两个组件具有独立的生命周期,projection需要是声明性的。 通过这种方式,父母或孩子可以更改投递的内容或投递方式,而无需重新渲染对方。

使用 children 方法,组件可以以无穷无尽的方式强制修改 children。 这将使得构建一个不会强制重新渲染父组件和子组件的框架变得极其困难。

Made with ❤️ by