Skip to content

Commit 66b071f

Browse files
authored
fix: correctly render array of custom elements in a slot (#305)
* update code * update test case * add test
1 parent da11739 commit 66b071f

File tree

2 files changed

+59
-62
lines changed

2 files changed

+59
-62
lines changed

src/runtime/composables/useDrupalCe/index.ts

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { defu } from 'defu'
22
import { appendResponseHeader } from 'h3'
33
import type { $Fetch, NitroFetchRequest } from 'nitropack'
4-
import type { Ref, ComputedRef } from 'vue'
4+
import type { Ref, ComputedRef, Component } from 'vue'
55
import { getDrupalBaseUrl, getMenuBaseUrl } from './server'
66
import type { UseFetchOptions } from '#app'
77
import { callWithNuxt } from '#app'
@@ -271,21 +271,22 @@ export const useDrupalCe = () => {
271271
*
272272
* @param customElements - Custom element data that can be:
273273
* - null/undefined (returns null, skipping render)
274-
* - string (auto-creates component rendering string as HTML if it contains markup)
274+
* - string (rendered inside a wrapping div element)
275275
* - single custom element object with {element: string, ...props}
276-
* - array of custom element objects
277-
* @returns Vue component definition, null for skipped render, or HTML-capable component for strings.
278-
* Result can be used directly with Vue's dynamic component: <component :is="result">
276+
* - array of strings or custom element objects (rendered inside a wrapping div element)
277+
* @returns Component | null - A Vue component that can be used with <component :is="component" />.
278+
* Returns null for skipped render, otherwise returns a Vue component
279+
* (either a custom element component or a wrapping div component for strings/arrays).
279280
*/
280281
const renderCustomElements = (
281-
customElements: null | undefined | string | Record<string, any> | Array<object>,
282-
) => {
282+
customElements: null | undefined | string | Record<string, any> | Array<string | object>,
283+
): Component | null => {
283284
// Handle null/undefined case
284285
if (customElements == null) {
285286
return null
286287
}
287288

288-
// Handle string case by creating a component that can render HTML
289+
// Handle string case by creating a component with wrapping div
289290
if (typeof customElements === 'string') {
290291
return defineComponent({
291292
setup() {
@@ -297,14 +298,21 @@ export const useDrupalCe = () => {
297298
}
298299

299300
// Handle empty object case
300-
if (typeof customElements === 'object' && Object.keys(customElements).length === 0) {
301+
if (Object.keys(customElements).length === 0) {
301302
return null
302303
}
303304

304-
// Handle array of custom elements
305+
// Handle array case by creating a wrapper div component that renders all children
305306
if (Array.isArray(customElements)) {
306-
return customElements.map((customElement) => {
307-
return renderCustomElements(customElement)
307+
return defineComponent({
308+
setup() {
309+
return () => h('div', {},
310+
customElements.map(element => {
311+
const rendered = renderCustomElements(element)
312+
return rendered ? h(rendered) : null
313+
})
314+
)
315+
}
308316
})
309317
}
310318

test/unit/renderCustomElements.test.ts

Lines changed: 39 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -3,160 +3,149 @@ import { describe, it, expect } from 'vitest'
33
import { mountSuspended } from '@nuxt/test-utils/runtime'
44
import { defineComponent } from 'vue'
55
import { useDrupalCe } from '../../src/runtime/composables/useDrupalCe'
6+
import {useNuxtApp} from "#imports";
67

78
describe('renderCustomElements', () => {
89
const { renderCustomElements } = useDrupalCe()
910

1011
// Define reusable test components
1112
const TestComponent = defineComponent({
13+
name: 'TestComponent',
1214
props: {
1315
foo: String
1416
},
1517
template: '<div>Test Component: {{ foo }}</div>'
1618
})
1719

1820
const AnotherComponent = defineComponent({
21+
name: 'AnotherComponent',
1922
props: {
2023
bar: String
2124
},
2225
template: '<div>Another Component: {{ bar }}</div>'
2326
})
27+
const app = useNuxtApp()
28+
app.vueApp.component('TestComponent', TestComponent)
29+
app.vueApp.component('AnotherComponent', AnotherComponent)
2430

2531
describe('basic input handling', () => {
26-
const NullRenderer = defineComponent({
27-
setup() {
28-
return { component: renderCustomElements(null) }
29-
},
30-
template: '<component :is="component" />'
31-
})
32-
3332
it('should return null for empty inputs', () => {
3433
expect(renderCustomElements(null)).toBe(null)
3534
expect(renderCustomElements(undefined)).toBe(null)
3635
expect(renderCustomElements({})).toBe(null)
3736
})
3837

3938
it('should render nothing when component is null', async () => {
40-
const wrapper = await mountSuspended(NullRenderer)
39+
const wrapper = await mountSuspended(defineComponent({
40+
setup() {
41+
return { component: renderCustomElements(null) }
42+
},
43+
template: '<component :is="component" />'
44+
}))
4145
expect(wrapper.html()).toBe('')
4246
})
4347
})
4448

4549
describe('string rendering', () => {
4650
it('should render plain text', async () => {
47-
const TextRenderer = defineComponent({
51+
const wrapper = await mountSuspended(defineComponent({
4852
setup() {
4953
return { component: renderCustomElements('Hello World') }
5054
},
5155
template: '<component :is="component" />'
52-
})
53-
const wrapper = await mountSuspended(TextRenderer)
56+
}))
5457
expect(wrapper.text()).toBe('Hello World')
5558
})
5659

5760
it('should render HTML string preserving markup', async () => {
5861
const htmlString = '<p>Hello <strong>World</strong></p>'
59-
const HtmlRenderer = defineComponent({
62+
const wrapper = await mountSuspended(defineComponent({
6063
setup() {
6164
return { component: renderCustomElements(htmlString) }
6265
},
6366
template: '<component :is="component" />'
64-
})
65-
const wrapper = await mountSuspended(HtmlRenderer)
67+
}))
6668
expect(wrapper.html()).toContain(htmlString)
6769
expect(wrapper.text()).toBe('Hello World')
6870
})
6971
})
7072

7173
describe('custom element rendering', () => {
7274
it('should render a single custom element', async () => {
73-
const ComponentRenderer = defineComponent({
74-
components: { TestComponent },
75+
const wrapper = await mountSuspended(defineComponent({
7576
setup() {
7677
return { component: renderCustomElements({
7778
element: 'test-component',
7879
foo: 'bar'
7980
})}
8081
},
8182
template: '<component :is="component" />'
82-
})
83-
const wrapper = await mountSuspended(ComponentRenderer)
83+
}))
8484
expect(wrapper.text()).toBe('Test Component: bar')
8585
})
8686
})
8787

8888
describe('array handling', () => {
89-
it('should render array of strings', async () => {
90-
const StringArrayRenderer = defineComponent({
89+
it('should render array of strings in a wrapper div', async () => {
90+
const wrapper = await mountSuspended(defineComponent({
9191
setup() {
92-
const content = ['Text 1', '<p>Text 2</p>']
93-
return {
94-
components: content.map(item => renderCustomElements(item))
95-
}
92+
return { component: renderCustomElements(['Text 1', '<p>Text 2</p>']) }
9693
},
97-
template: '<div><component v-for="comp in components" :is="comp" /></div>'
98-
})
99-
const wrapper = await mountSuspended(StringArrayRenderer)
94+
template: '<component :is="component" />'
95+
}))
96+
expect(wrapper.html()).toContain('<div>')
10097
expect(wrapper.text()).toContain('Text 1')
10198
expect(wrapper.text()).toContain('Text 2')
10299
expect(wrapper.html()).toContain('<p>Text 2</p>')
103100
})
104101

105-
it('should render array of custom elements', async () => {
106-
const ElementArrayRenderer = defineComponent({
107-
components: { TestComponent, AnotherComponent },
102+
it('should render array of custom elements in a wrapper div', async () => {
103+
const wrapper = await mountSuspended(defineComponent({
108104
setup() {
109-
const content = [
110-
{ element: 'test-component', foo: 'one' },
111-
{ element: 'another-component', bar: 'two' }
112-
]
113-
return {
114-
components: content.map(item => renderCustomElements(item))
115-
}
105+
return { component: renderCustomElements([
106+
{ element: 'test-component', foo: 'one' },
107+
{ element: 'another-component', bar: 'two' }
108+
]) }
116109
},
117-
template: '<div><component v-for="comp in components" :is="comp" /></div>'
118-
})
119-
const wrapper = await mountSuspended(ElementArrayRenderer)
110+
template: '<component :is="component" />'
111+
}))
112+
expect(wrapper.html()).toContain('<div>')
120113
expect(wrapper.text()).toContain('Test Component: one')
121114
expect(wrapper.text()).toContain('Another Component: two')
122115
})
123116
})
124117

125118
describe('edge cases', () => {
126119
it('should handle malformed element objects', async () => {
127-
const MalformedRenderer = defineComponent({
128-
components: { TestComponent },
120+
const wrapper = await mountSuspended(defineComponent({
129121
setup() {
130122
return { component: renderCustomElements({ element: 'test-component' })}
131123
},
132124
template: '<component :is="component" />'
133-
})
134-
const wrapper = await mountSuspended(MalformedRenderer)
125+
}))
135126
expect(wrapper.text()).toBe('Test Component:')
136127
})
137128

138129
it('should handle nonexistent components', async () => {
139-
const NonexistentRenderer = defineComponent({
130+
const wrapper = await mountSuspended(defineComponent({
140131
setup() {
141132
return { component: renderCustomElements({
142133
element: 'nonexistent-component',
143134
foo: 'bar'
144135
})}
145136
},
146137
template: '<component :is="component" />'
147-
})
148-
const wrapper = await mountSuspended(NonexistentRenderer)
138+
}))
149139
expect(wrapper.html()).toBe('')
150140
})
151141

152142
it('should handle empty arrays', async () => {
153-
const EmptyArrayRenderer = defineComponent({
143+
const wrapper = await mountSuspended(defineComponent({
154144
setup() {
155145
return { component: renderCustomElements([]) }
156146
},
157147
template: '<component :is="component" />'
158-
})
159-
const wrapper = await mountSuspended(EmptyArrayRenderer)
148+
}))
160149
expect(wrapper.html()).toBe('')
161150
})
162151
})

0 commit comments

Comments
 (0)