Skip to content

Commit

Permalink
feat: render slot contents (#43)
Browse files Browse the repository at this point in the history
* refactor: make tag name a computed property

* feat: render default slot content

* feat: render default slot content

* feat: render named slot content

* fix: Node -> Child

* fix: avoid referencing slots in computed
  • Loading branch information
ktsn committed Jul 1, 2018
1 parent 04bcabc commit c5a398d
Show file tree
Hide file tree
Showing 8 changed files with 225 additions and 36 deletions.
26 changes: 14 additions & 12 deletions src/view/components/Child.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script lang="ts">
import Vue, { VNode } from 'vue'
import NodeSlot from './NodeSlot.vue'
import ContainerNode from './ContainerNode.vue'
import Expression from './Expression.vue'
import { ElementChild } from '@/parser/template/types'
Expand Down Expand Up @@ -28,25 +29,26 @@ export default Vue.extend({
childComponents: {
type: Array as { (): ChildComponent[] },
required: true
},
slots: {
type: Object as { (): Record<string, VNode[]> },
required: true
}
},
// @ts-ignore
render(h, { props, listeners }): VNode {
const { uri, data, scope, childComponents } = props
render(h, { props, listeners }): any /* VNode | VNode[] */ {
const { data, scope } = props
switch (data.type) {
case 'Element':
return h(ContainerNode, {
props: {
uri,
data,
scope,
childComponents
},
on: listeners
const slot = data.startTag.attributes.find(attr => attr.name === 'slot')
return h(data.name === 'slot' ? NodeSlot : ContainerNode, {
props,
on: listeners,
slot: slot && slot.value
})
case 'TextNode':
return [data.text] as any
return [data.text]
case 'ExpressionNode':
return h(Expression, {
props: {
Expand Down
7 changes: 6 additions & 1 deletion src/view/components/ContainerNode.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
</template>

<script lang="ts">
import Vue from 'vue'
import Vue, { VNode } from 'vue'
import Node from './Node.vue'
import { Element } from '@/parser/template/types'
import { DefaultValue, ChildComponent } from '@/parser/script/types'
Expand Down Expand Up @@ -42,6 +42,11 @@ export default Vue.extend({
childComponents: {
type: Array as { (): ChildComponent[] },
required: true
},
slots: {
type: Object as { (): Record<string, VNode[]> },
required: true
}
},
Expand Down
47 changes: 30 additions & 17 deletions src/view/components/ContainerVueComponent.vue
Original file line number Diff line number Diff line change
@@ -1,17 +1,5 @@
<template>
<VueComponent
v-if="document"
:uri="uri"
:template="document.template"
:scope="scope"
:child-components="document.childComponents"
:styles="document.styleCode"
:props-data="propsData"
/>
</template>

<script lang="ts">
import Vue from 'vue'
import Vue, { VNode } from 'vue'
import VueComponent from './VueComponent.vue'
import { ScopedDocument, DocumentScope } from '../store/modules/project/types'
import { mapper } from '../store'
Expand All @@ -21,10 +9,6 @@ const projectMapper = mapper.module('project')
export default Vue.extend({
name: 'ContainerVueComponent',
beforeCreate() {
this.$options.components!.VueComponent = VueComponent
},
props: {
uri: {
type: String,
Expand Down Expand Up @@ -53,6 +37,35 @@ export default Vue.extend({
scope(): DocumentScope | undefined {
return this.scopes[this.uri]
}
},
render(h): VNode {
if (!this.document) return h()
const flattenSlots = Object.keys(this.$slots).map(name => {
return h(
'template',
{
slot: name
},
this.$slots[name]
)
})
return h(
VueComponent,
{
props: {
uri: this.uri,
template: this.document.template,
scope: this.scope,
childComponents: this.document.childComponents,
styles: this.document.styleCode,
propsData: this.propsData
}
},
flattenSlots
)
}
})
</script>
24 changes: 19 additions & 5 deletions src/view/components/Node.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,23 @@ export default Vue.extend({
type: Array as { (): ChildComponent[] },
required: true
},
slots: {
type: Object as { (): Record<string, VNode[]> },
required: true
},
selectable: Boolean,
selected: Boolean
},
computed: {
vnodeTag(): string | typeof Vue {
if (this.nodeUri) {
return ContainerVueComponent
}
return this.data.name
},
vnodeData(): VNodeData {
const { data: node, scope, selectable } = this
const data = convertToVNodeData(
Expand All @@ -54,8 +66,8 @@ export default Vue.extend({
}
}
// If there is matched nodeUri, the vnode will be ContainerVueComponent
if (this.nodeUri) {
const tag = this.vnodeTag
if (tag === ContainerVueComponent) {
data.props = {
uri: this.nodeUri,
propsData: data.attrs
Expand Down Expand Up @@ -144,18 +156,20 @@ export default Vue.extend({
},
render(h): VNode {
const { uri, data, childComponents } = this
const { uri, childComponents, slots } = this
return h(
this.nodeUri ? ContainerVueComponent : data.name,
this.vnodeTag,
this.vnodeData,
this.resolvedChildren.map(c => {
// Slot name will be resolved in <Child> component
return h(Child, {
props: {
uri,
data: c.el,
scope: c.scope,
childComponents
childComponents,
slots
},
on: this.$listeners
})
Expand Down
57 changes: 57 additions & 0 deletions src/view/components/NodeSlot.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<script lang="ts">
import Vue, { VNode } from 'vue'
import Child from './Child.vue'
import { Element } from '@/parser/template/types'
import { DefaultValue, ChildComponent } from '@/parser/script/types'
export default Vue.extend({
name: 'NodeSlot',
functional: true,
props: {
uri: {
type: String,
required: true
},
data: {
type: Object as { (): Element },
required: true
},
scope: {
type: Object as { (): Record<string, DefaultValue> },
required: true
},
childComponents: {
type: Array as { (): ChildComponent[] },
required: true
},
slots: {
type: Object as { (): Record<string, VNode[]> },
required: true
}
},
render(h, { props }): any /* VNode[] */ {
const { data, slots } = props
const slotAttr = data.startTag.attributes.find(attr => attr.name === 'name')
const slot = slots[slotAttr ? slotAttr.value : 'default']
if (!slot) {
// placeholder content
return props.data.children.map(child => {
return h(Child, {
props: {
uri: props.uri,
data: child,
scope: props.scope,
childComponents: props.childComponents,
slots
}
})
})
}
return slot
}
})
</script>
3 changes: 2 additions & 1 deletion src/view/components/VueComponent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ export default Vue.extend({
uri: this.uri,
data: child.el,
scope: child.scope,
childComponents: this.childComponents
childComponents: this.childComponents,
slots: this.$slots
},
on: {
select: (path: number[]) => {
Expand Down
7 changes: 7 additions & 0 deletions test/view/VueComponent/__snapshots__/slot.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`VueComponent slot renders named slot content 1`] = `"<div><style></style><div class=\\"\\"><style></style><div data-scope-scope-id=\\"\\" class=\\"\\"><p data-scope-scope-id=\\"\\" class=\\"\\">foo content</p><p slot=\\"test\\" class=\\"\\">named</p></div></div></div>"`;
exports[`VueComponent slot renders placeholder slot content 1`] = `"<div><style></style><div class=\\"\\"><p class=\\"\\">placeholder content</p></div></div>"`;
exports[`VueComponent slot renders slot content 1`] = `"<div><style></style><div class=\\"\\"><style></style><div data-scope-scope-id=\\"\\" class=\\"\\"><p data-scope-scope-id=\\"\\" class=\\"\\">foo content</p><p class=\\"\\">injected</p></div></div></div>"`;
90 changes: 90 additions & 0 deletions test/view/VueComponent/slot.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { createTemplate, h, render, a } from '../../helpers/template'

describe('VueComponent slot', () => {
it('renders placeholder slot content', () => {
// prettier-ignore
const template = createTemplate([
h('div', [], [
h('slot', [], [
h('p', [], ['placeholder content'])
])
])
])

const wrapper = render(template)
expect(wrapper.html()).toMatchSnapshot()
})

it('renders slot content', () => {
// prettier-ignore
const template = createTemplate([
h('Foo', [], [
h('p', [], ['injected'])
])
])

const components = {
'file://Foo.vue': {
// prettier-ignore
template: createTemplate([
h('div', [], [
h('p', [], ['foo content']),
h('slot', [], [])
])
])
}
}

const wrapper = render(
template,
[],
[],
[
{
name: 'Foo',
uri: 'file://Foo.vue'
}
],
components
)

expect(wrapper.html()).toMatchSnapshot()
})

it('renders named slot content', () => {
// prettier-ignore
const template = createTemplate([
h('Foo', [], [
h('p', [], ['default']),
h('p', [a('slot', 'test')], ['named'])
])
])

const components = {
'file://Foo.vue': {
// prettier-ignore
template: createTemplate([
h('div', [], [
h('p', [], ['foo content']),
h('slot', [a('name', 'test')], [])
])
])
}
}

const wrapper = render(
template,
[],
[],
[
{
name: 'Foo',
uri: 'file://Foo.vue'
}
],
components
)

expect(wrapper.html()).toMatchSnapshot()
})
})

0 comments on commit c5a398d

Please sign in to comment.