@contentful/experiences-svelte 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## 0.5.1 (2026-08-07)
2
+
3
+ ### 🚀 Features
4
+
5
+ - expose slot children as an array instead of a single wrapping node ([#122](https://github.com/contentful/experiences/pull/122))
6
+
1
7
  ## 0.5.0 (2026-08-07)
2
8
 
3
9
  ### 🩹 Fixes
package/README.md CHANGED
@@ -35,7 +35,7 @@ resolveExperience(payload, config, opts?); // Async; walks payload, runs resolve
35
35
  ServerExperienceRenderer; // SSR-safe; active viewport seeded from initialViewportId
36
36
  ClientExperienceRenderer; // Subscribes to window.matchMedia via runes
37
37
  MissingComponent; // Default fallback for unregistered component types
38
- NodesRenderer; // Exposed so you can render non-`children` slots manually
38
+ NodesRenderer; // Exposed so you can render non-`children` slots manually (see Slot children)
39
39
  useActiveViewport; // Rune-backed reactive object; you'll rarely need it directly
40
40
  ```
41
41
 
@@ -124,6 +124,37 @@ export const experienceConfig: Config = { components, resolveToken };
124
124
  <ServerExperienceRenderer experience={data.experience} config={experienceConfig} />
125
125
  ```
126
126
 
127
+ ### Slot children
128
+
129
+ A component's default slot children arrive as an **array of Snippets** (`Snippet[]`), one per child — not a single wrapping Snippet. For the common "just render them" case, iterate the array with `{#each}` and `{@render}`; to wrap, reorder, or drop children individually, do it inside that loop.
130
+
131
+ ```svelte
132
+ <!-- Section.svelte -->
133
+ <script lang="ts">
134
+ import type { Snippet } from 'svelte';
135
+
136
+ let { children }: { children?: Snippet[] } = $props();
137
+ </script>
138
+
139
+ <!-- Common case — render them all: -->
140
+ <div>
141
+ {#each children ?? [] as child}
142
+ {@render child()}
143
+ {/each}
144
+ </div>
145
+
146
+ <!-- Or take control of each child: -->
147
+ <!--
148
+ <div>
149
+ {#each children ?? [] as child, i}
150
+ <div class="cell">{@render child()}</div>
151
+ {/each}
152
+ </div>
153
+ -->
154
+ ```
155
+
156
+ The default slot is injected as the `children` prop. Any **additional named slots** are not auto-injected — reach them from the raw payload via `getContentfulComponent().slots` (a `Record<string, PortableRenderNode[]>`) and render them yourself with `NodesRenderer`.
157
+
127
158
  For the full getting-started walkthrough, the merge-precedence rules, viewport handling, and design rationale, see the [root README](../../README.md) and [`AGENTS.md`](../../AGENTS.md).
128
159
 
129
160
  ---
@@ -23,7 +23,7 @@
23
23
  config: Config;
24
24
  experience: RenderContext;
25
25
  renderUnknown: RenderUnknown;
26
- children: Snippet;
26
+ children: Snippet[];
27
27
  }
28
28
 
29
29
  let { node, config, experience, renderUnknown, children }: NodeRendererProps = $props();
@@ -7,7 +7,7 @@ interface NodeRendererProps {
7
7
  config: Config;
8
8
  experience: RenderContext;
9
9
  renderUnknown: RenderUnknown;
10
- children: Snippet;
10
+ children: Snippet[];
11
11
  }
12
12
  declare const NodeRenderer: import("svelte").Component<NodeRendererProps, {}, "">;
13
13
  type NodeRenderer = ReturnType<typeof NodeRenderer>;
@@ -1 +1 @@
1
- {"version":3,"file":"NodeRenderer.svelte.d.ts","sourceRoot":"","sources":["../src/NodeRenderer.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAI3E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAGH,KAAK,MAAM,EACX,KAAK,aAAa,EACnB,MAAM,YAAY,CAAC;AAGpB,UAAU,iBAAiB;IACzB,IAAI,EAAE,kBAAkB,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,aAAa,CAAC;IAC1B,aAAa,EAAE,aAAa,CAAC;IAC7B,QAAQ,EAAE,OAAO,CAAC;CACnB;AAoEH,QAAA,MAAM,YAAY,uDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
1
+ {"version":3,"file":"NodeRenderer.svelte.d.ts","sourceRoot":"","sources":["../src/NodeRenderer.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAI3E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAGH,KAAK,MAAM,EACX,KAAK,aAAa,EACnB,MAAM,YAAY,CAAC;AAGpB,UAAU,iBAAiB;IACzB,IAAI,EAAE,kBAAkB,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,aAAa,CAAC;IAC1B,aAAa,EAAE,aAAa,CAAC;IAC7B,QAAQ,EAAE,OAAO,EAAE,CAAC;CACrB;AAoEH,QAAA,MAAM,YAAY,uDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
@@ -1,22 +1,17 @@
1
1
  <!--
2
2
  * Recursive renderer over PortableRenderNodes. Customer components receive
3
3
  * the merged content / design / resolveData prop bag plus the default
4
- * `children` slot as a named Snippet:
4
+ * `children` slot as an ARRAY of Snippets — one zero-arg Snippet per
5
+ * pre-rendered child node — declared as `children?: Snippet[]`. A component
6
+ * renders them all with an each-block + @render, or maps/filters/wraps the
7
+ * children individually. The array mirrors the React adapter's ReactNode[]
8
+ * slot contract; see the README for the consuming-a-slot example.
5
9
  *
6
- * <script lang="ts">
7
- * import type { Snippet } from 'svelte';
8
- * let { text, children }: { text?: string; children?: Snippet } = $props();
9
- * </script>
10
- * <button>
11
- * {text}
12
- * {@render children?.()}
13
- * </button>
14
- *
15
- * Only the `children` slot is rendered automatically — Svelte 5 Snippets are
16
- * compile-time entities, so the renderer can't synthesize an unbounded set
17
- * of named Snippet props from runtime payload data. Additional named slots
18
- * are reachable through `getContentfulComponent().slots` and can be rendered
19
- * with the exported `<NodesRenderer />` (see README).
10
+ * Only the default `children` slot is injected as a prop — Svelte 5 Snippets
11
+ * are compile-time entities, so the renderer can't synthesize an unbounded
12
+ * set of named Snippet props from runtime payload data. Additional named
13
+ * slots are reachable through `getContentfulComponent().slots` and can be
14
+ * rendered with the exported NodesRenderer (see README).
20
15
  *
21
16
  * The Experience runtime context and the raw Contentful payload are read
22
17
  * via `getExperience` / `getContentfulComponent`.
@@ -25,10 +20,11 @@
25
20
  * the active viewport is sourced (initial seed vs reactive matchMedia).
26
21
  -->
27
22
  <script lang="ts">
23
+ import type { Snippet } from 'svelte';
24
+
28
25
  import type { PortableRenderNode } from '@contentful/experiences-sdk-core';
29
26
 
30
27
  import NodeRenderer from './NodeRenderer.svelte';
31
- import NodesRenderer from './NodesRenderer.svelte';
32
28
  import type { RenderUnknown } from './component-props.js';
33
29
  import type { Config, RenderContext } from './types.js';
34
30
 
@@ -40,12 +36,24 @@
40
36
  }
41
37
 
42
38
  let { nodes, config, experience, renderUnknown }: NodesRendererProps = $props();
39
+
40
+ // Curry the parameterized `renderNode` snippet into a zero-arg Snippet bound
41
+ // to one node. Svelte 5 invokes a snippet as `(anchor, ...argGetters)`, so we
42
+ // forward the anchor and supply the node through a getter. This is the one
43
+ // spot that leans on Svelte's snippet calling convention; pinned by the
44
+ // `children`-array tests in server-renderer.test.ts.
45
+ function toChildSnippet(node: PortableRenderNode): Snippet {
46
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- programmatic snippet construction; see note above.
47
+ return ((anchor: unknown) => (renderNode as any)(anchor, () => node)) as Snippet;
48
+ }
43
49
  </script>
44
50
 
45
- {#each nodes as node, index (node.nodeId ?? index)}
46
- {#snippet children()}
47
- {@const childrenNodes = (node.slots.children ?? []) as PortableRenderNode[]}
48
- <NodesRenderer nodes={childrenNodes} {config} {experience} {renderUnknown} />
49
- {/snippet}
51
+ {#snippet renderNode(node: PortableRenderNode)}
52
+ {@const childNodes = (node.slots.children ?? []) as PortableRenderNode[]}
53
+ {@const children = childNodes.map(toChildSnippet)}
50
54
  <NodeRenderer {node} {config} {experience} {renderUnknown} {children} />
55
+ {/snippet}
56
+
57
+ {#each nodes as node, index (node.nodeId ?? index)}
58
+ {@render renderNode(node)}
51
59
  {/each}
@@ -1,5 +1,4 @@
1
1
  import type { PortableRenderNode } from '@contentful/experiences-sdk-core';
2
- import NodesRenderer from './NodesRenderer.svelte';
3
2
  import type { RenderUnknown } from './component-props.js';
4
3
  import type { Config, RenderContext } from './types.js';
5
4
  interface NodesRendererProps {
@@ -1 +1 @@
1
- {"version":3,"file":"NodesRenderer.svelte.d.ts","sourceRoot":"","sources":["../src/NodesRenderer.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAG3E,OAAO,aAAa,MAAM,wBAAwB,CAAC;AACnD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAGtD,UAAU,kBAAkB;IAC1B,KAAK,EAAE,kBAAkB,EAAE,CAAC;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,aAAa,CAAC;IAC1B,aAAa,EAAE,aAAa,CAAC;CAC9B;AAwBH,QAAA,MAAM,aAAa,wDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
1
+ {"version":3,"file":"NodesRenderer.svelte.d.ts","sourceRoot":"","sources":["../src/NodesRenderer.svelte.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAG3E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAGtD,UAAU,kBAAkB;IAC1B,KAAK,EAAE,kBAAkB,EAAE,CAAC;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,aAAa,CAAC;IAC1B,aAAa,EAAE,aAAa,CAAC;CAC9B;AAqCH,QAAA,MAAM,aAAa,wDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
@@ -9,6 +9,7 @@ import HeadingFixture from './test-fixtures/HeadingFixture.svelte';
9
9
  import ItemFixture from './test-fixtures/ItemFixture.svelte';
10
10
  import PrecedenceFixture from './test-fixtures/PrecedenceFixture.svelte';
11
11
  import ExperienceTemplateFixture from './test-fixtures/ExperienceTemplateFixture.svelte';
12
+ import WrappingContainerFixture from './test-fixtures/WrappingContainerFixture.svelte';
12
13
  import { captureSink } from './test-fixtures/capture-sink.js';
13
14
  import { toCss } from './design-utils.js';
14
15
  const VIEWPORTS = [
@@ -313,6 +314,66 @@ describe('ServerExperienceRenderer', () => {
313
314
  warn.mockRestore();
314
315
  });
315
316
  });
317
+ describe('ServerExperienceRenderer — slot children as an array', () => {
318
+ const childrenPayload = () => ({
319
+ viewports: VIEWPORTS,
320
+ nodes: [
321
+ componentNode('contentful-container', {
322
+ id: 'c',
323
+ slots: {
324
+ children: [
325
+ componentNode('contentful-heading', {
326
+ id: 'a',
327
+ contentProperties: { text: 'one' },
328
+ }),
329
+ componentNode('contentful-heading', {
330
+ id: 'b',
331
+ contentProperties: { text: 'two' },
332
+ }),
333
+ componentNode('contentful-heading', {
334
+ id: 'd',
335
+ contentProperties: { text: 'three' },
336
+ }),
337
+ ],
338
+ },
339
+ }),
340
+ ],
341
+ });
342
+ it('passes slot children as a Snippet[] a component can wrap individually', async () => {
343
+ const cfg = {
344
+ components: {
345
+ 'contentful-container': WrappingContainerFixture,
346
+ 'contentful-heading': HeadingFixture,
347
+ },
348
+ };
349
+ const plan = await resolveExperience(childrenPayload(), cfg);
350
+ const { container } = render(ServerExperienceRenderer, {
351
+ props: { experience: plan, config: cfg },
352
+ });
353
+ // The component received a real array of the right length.
354
+ expect(captureSink[0].props.childrenIsArray).toBe(true);
355
+ expect(captureSink[0].props.childCount).toBe(3);
356
+ // Each child got its own wrapper — proves per-child rendering control.
357
+ const wrappers = container.querySelectorAll('.wrap');
358
+ expect(wrappers.length).toBe(3);
359
+ expect(container.querySelector('[data-index="0"]')).not.toBeNull();
360
+ expect(container.querySelector('[data-index="2"]')).not.toBeNull();
361
+ expect(container.textContent).toContain('one');
362
+ expect(container.textContent).toContain('three');
363
+ });
364
+ it('renders an empty container when the slot has no children', async () => {
365
+ const cfg = {
366
+ components: { 'contentful-container': WrappingContainerFixture },
367
+ };
368
+ const plan = await resolveExperience({ viewports: VIEWPORTS, nodes: [componentNode('contentful-container', { id: 'c' })] }, cfg);
369
+ const { container } = render(ServerExperienceRenderer, {
370
+ props: { experience: plan, config: cfg },
371
+ });
372
+ expect(captureSink[0].props.childCount).toBe(0);
373
+ expect(container.querySelectorAll('.wrap').length).toBe(0);
374
+ expect(container.querySelector('[data-container]')).not.toBeNull();
375
+ });
376
+ });
316
377
  describe('ServerExperienceRenderer — bare-component registrations', () => {
317
378
  it('accepts a bare Svelte component as a registry entry', async () => {
318
379
  const cfg = { components: { item: PrecedenceFixture } };
@@ -3,10 +3,12 @@
3
3
 
4
4
  import { getDesignValues } from '../get-design-values.js';
5
5
 
6
- let { children }: { children?: Snippet } = $props();
6
+ let { children }: { children?: Snippet[] } = $props();
7
7
  const design = $derived(getDesignValues());
8
8
  </script>
9
9
 
10
10
  <div data-padding={design.cfPadding as string}>
11
- {#if children}{@render children()}{/if}
11
+ {#each children ?? [] as child}
12
+ {@render child()}
13
+ {/each}
12
14
  </div>
@@ -1,6 +1,6 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  type $$ComponentProps = {
3
- children?: Snippet;
3
+ children?: Snippet[];
4
4
  };
5
5
  declare const ContainerFixture: import("svelte").Component<$$ComponentProps, {}, "">;
6
6
  type ContainerFixture = ReturnType<typeof ContainerFixture>;
@@ -1 +1 @@
1
- {"version":3,"file":"ContainerFixture.svelte.d.ts","sourceRoot":"","sources":["../../src/test-fixtures/ContainerFixture.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAIrC,KAAK,gBAAgB,GAAI;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAgBjD,QAAA,MAAM,gBAAgB,sDAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
1
+ {"version":3,"file":"ContainerFixture.svelte.d.ts","sourceRoot":"","sources":["../../src/test-fixtures/ContainerFixture.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAIrC,KAAK,gBAAgB,GAAI;IAAE,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAA;CAAE,CAAC;AAkBnD,QAAA,MAAM,gBAAgB,sDAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
@@ -0,0 +1,27 @@
1
+ <!--
2
+ Test fixture: proves slot children arrive as an array of Snippets that a
3
+ component can iterate and wrap individually. Also records the received array
4
+ length in the capture sink.
5
+ -->
6
+ <script lang="ts">
7
+ import type { Snippet } from 'svelte';
8
+
9
+ import { getContentfulComponent, getExperience } from '../context.js';
10
+ import { captureSink } from './capture-sink.js';
11
+
12
+ let { children }: { children?: Snippet[] } = $props();
13
+
14
+ captureSink.push({
15
+ props: { childCount: children?.length ?? 0, childrenIsArray: Array.isArray(children) },
16
+ experience: getExperience(),
17
+ contentful: getContentfulComponent(),
18
+ });
19
+ </script>
20
+
21
+ <div data-container>
22
+ {#each children ?? [] as child, i}
23
+ <div class="wrap" data-index={i}>
24
+ {@render child()}
25
+ </div>
26
+ {/each}
27
+ </div>
@@ -0,0 +1,8 @@
1
+ import type { Snippet } from 'svelte';
2
+ type $$ComponentProps = {
3
+ children?: Snippet[];
4
+ };
5
+ declare const WrappingContainerFixture: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type WrappingContainerFixture = ReturnType<typeof WrappingContainerFixture>;
7
+ export default WrappingContainerFixture;
8
+ //# sourceMappingURL=WrappingContainerFixture.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WrappingContainerFixture.svelte.d.ts","sourceRoot":"","sources":["../../src/test-fixtures/WrappingContainerFixture.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAKrC,KAAK,gBAAgB,GAAI;IAAE,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAA;CAAE,CAAC;AA2BnD,QAAA,MAAM,wBAAwB,sDAAwC,CAAC;AACvE,KAAK,wBAAwB,GAAG,UAAU,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAC5E,eAAe,wBAAwB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentful/experiences-svelte",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Svelte adapter for Contentful Experiences — Server and Client renderers over a PortableRenderPlan",
5
5
  "license": "MIT",
6
6
  "type": "module",