Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: correctly render array of custom elements in a slot #305

Merged
merged 3 commits into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 20 additions & 12 deletions src/runtime/composables/useDrupalCe/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { defu } from 'defu'
import { appendResponseHeader } from 'h3'
import type { $Fetch, NitroFetchRequest } from 'nitropack'
import type { Ref, ComputedRef } from 'vue'
import type { Ref, ComputedRef, Component } from 'vue'
import { getDrupalBaseUrl, getMenuBaseUrl } from './server'
import type { UseFetchOptions } from '#app'
import { callWithNuxt } from '#app'
Expand Down Expand Up @@ -271,21 +271,22 @@ export const useDrupalCe = () => {
*
* @param customElements - Custom element data that can be:
* - null/undefined (returns null, skipping render)
* - string (auto-creates component rendering string as HTML if it contains markup)
* - string (rendered inside a wrapping div element)
* - single custom element object with {element: string, ...props}
* - array of custom element objects
* @returns Vue component definition, null for skipped render, or HTML-capable component for strings.
* Result can be used directly with Vue's dynamic component: <component :is="result">
* - array of strings or custom element objects (rendered inside a wrapping div element)
* @returns Component | null - A Vue component that can be used with <component :is="component" />.
* Returns null for skipped render, otherwise returns a Vue component
* (either a custom element component or a wrapping div component for strings/arrays).
*/
const renderCustomElements = (
customElements: null | undefined | string | Record<string, any> | Array<object>,
) => {
customElements: null | undefined | string | Record<string, any> | Array<string | object>,
): Component | null => {
// Handle null/undefined case
if (customElements == null) {
return null
}

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

// Handle empty object case
if (typeof customElements === 'object' && Object.keys(customElements).length === 0) {
if (Object.keys(customElements).length === 0) {
return null
}

// Handle array of custom elements
// Handle array case by creating a wrapper div component that renders all children
if (Array.isArray(customElements)) {
return customElements.map((customElement) => {
return renderCustomElements(customElement)
return defineComponent({
setup() {
return () => h('div', {},
customElements.map(element => {
const rendered = renderCustomElements(element)
return rendered ? h(rendered) : null
})
)
}
})
}

Expand Down
89 changes: 39 additions & 50 deletions test/unit/renderCustomElements.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,160 +3,149 @@ import { describe, it, expect } from 'vitest'
import { mountSuspended } from '@nuxt/test-utils/runtime'
import { defineComponent } from 'vue'
import { useDrupalCe } from '../../src/runtime/composables/useDrupalCe'
import {useNuxtApp} from "#imports";

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

// Define reusable test components
const TestComponent = defineComponent({
name: 'TestComponent',
props: {
foo: String
},
template: '<div>Test Component: {{ foo }}</div>'
})

const AnotherComponent = defineComponent({
name: 'AnotherComponent',
props: {
bar: String
},
template: '<div>Another Component: {{ bar }}</div>'
})
const app = useNuxtApp()
app.vueApp.component('TestComponent', TestComponent)
app.vueApp.component('AnotherComponent', AnotherComponent)

describe('basic input handling', () => {
const NullRenderer = defineComponent({
setup() {
return { component: renderCustomElements(null) }
},
template: '<component :is="component" />'
})

it('should return null for empty inputs', () => {
expect(renderCustomElements(null)).toBe(null)
expect(renderCustomElements(undefined)).toBe(null)
expect(renderCustomElements({})).toBe(null)
})

it('should render nothing when component is null', async () => {
const wrapper = await mountSuspended(NullRenderer)
const wrapper = await mountSuspended(defineComponent({
setup() {
return { component: renderCustomElements(null) }
},
template: '<component :is="component" />'
}))
expect(wrapper.html()).toBe('')
})
})

describe('string rendering', () => {
it('should render plain text', async () => {
const TextRenderer = defineComponent({
const wrapper = await mountSuspended(defineComponent({
setup() {
return { component: renderCustomElements('Hello World') }
},
template: '<component :is="component" />'
})
const wrapper = await mountSuspended(TextRenderer)
}))
expect(wrapper.text()).toBe('Hello World')
})

it('should render HTML string preserving markup', async () => {
const htmlString = '<p>Hello <strong>World</strong></p>'
const HtmlRenderer = defineComponent({
const wrapper = await mountSuspended(defineComponent({
setup() {
return { component: renderCustomElements(htmlString) }
},
template: '<component :is="component" />'
})
const wrapper = await mountSuspended(HtmlRenderer)
}))
expect(wrapper.html()).toContain(htmlString)
expect(wrapper.text()).toBe('Hello World')
})
})

describe('custom element rendering', () => {
it('should render a single custom element', async () => {
const ComponentRenderer = defineComponent({
components: { TestComponent },
const wrapper = await mountSuspended(defineComponent({
setup() {
return { component: renderCustomElements({
element: 'test-component',
foo: 'bar'
})}
},
template: '<component :is="component" />'
})
const wrapper = await mountSuspended(ComponentRenderer)
}))
expect(wrapper.text()).toBe('Test Component: bar')
})
})

describe('array handling', () => {
it('should render array of strings', async () => {
const StringArrayRenderer = defineComponent({
it('should render array of strings in a wrapper div', async () => {
const wrapper = await mountSuspended(defineComponent({
setup() {
const content = ['Text 1', '<p>Text 2</p>']
return {
components: content.map(item => renderCustomElements(item))
}
return { component: renderCustomElements(['Text 1', '<p>Text 2</p>']) }
},
template: '<div><component v-for="comp in components" :is="comp" /></div>'
})
const wrapper = await mountSuspended(StringArrayRenderer)
template: '<component :is="component" />'
}))
expect(wrapper.html()).toContain('<div>')
expect(wrapper.text()).toContain('Text 1')
expect(wrapper.text()).toContain('Text 2')
expect(wrapper.html()).toContain('<p>Text 2</p>')
})

it('should render array of custom elements', async () => {
const ElementArrayRenderer = defineComponent({
components: { TestComponent, AnotherComponent },
it('should render array of custom elements in a wrapper div', async () => {
const wrapper = await mountSuspended(defineComponent({
setup() {
const content = [
{ element: 'test-component', foo: 'one' },
{ element: 'another-component', bar: 'two' }
]
return {
components: content.map(item => renderCustomElements(item))
}
return { component: renderCustomElements([
{ element: 'test-component', foo: 'one' },
{ element: 'another-component', bar: 'two' }
]) }
},
template: '<div><component v-for="comp in components" :is="comp" /></div>'
})
const wrapper = await mountSuspended(ElementArrayRenderer)
template: '<component :is="component" />'
}))
expect(wrapper.html()).toContain('<div>')
expect(wrapper.text()).toContain('Test Component: one')
expect(wrapper.text()).toContain('Another Component: two')
})
})

describe('edge cases', () => {
it('should handle malformed element objects', async () => {
const MalformedRenderer = defineComponent({
components: { TestComponent },
const wrapper = await mountSuspended(defineComponent({
setup() {
return { component: renderCustomElements({ element: 'test-component' })}
},
template: '<component :is="component" />'
})
const wrapper = await mountSuspended(MalformedRenderer)
}))
expect(wrapper.text()).toBe('Test Component:')
})

it('should handle nonexistent components', async () => {
const NonexistentRenderer = defineComponent({
const wrapper = await mountSuspended(defineComponent({
setup() {
return { component: renderCustomElements({
element: 'nonexistent-component',
foo: 'bar'
})}
},
template: '<component :is="component" />'
})
const wrapper = await mountSuspended(NonexistentRenderer)
}))
expect(wrapper.html()).toBe('')
})

it('should handle empty arrays', async () => {
const EmptyArrayRenderer = defineComponent({
const wrapper = await mountSuspended(defineComponent({
setup() {
return { component: renderCustomElements([]) }
},
template: '<component :is="component" />'
})
const wrapper = await mountSuspended(EmptyArrayRenderer)
}))
expect(wrapper.html()).toBe('')
})
})
Expand Down
Loading