Skip to content
This repository was archived by the owner on Mar 8, 2019. It is now read-only.

Commit 8ad6f95

Browse files
authored
feat: add slot support in jsx render function (#101)
* add e2e tests and slotHandler first fixes * slot support unit tests * add description of slots in jsx * fix e2e test * Update src/script-handlers/slotHandler.ts Co-Authored-By: elevatebart <[email protected]> * fix code changes * fix description true if no description at all * last fixes
1 parent 821cdbc commit 8ad6f95

File tree

5 files changed

+148
-6
lines changed

5 files changed

+148
-6
lines changed

src/script-handlers/__tests__/slotHandler.ts

+72-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ describe('render function slotHandler', () => {
1717

1818
beforeEach(() => {
1919
mockSlotDescriptor = { description: '' }
20-
documentation = new (require('../../Documentation')).Documentation()
20+
documentation = new Documentation()
2121
const mockGetSlotDescriptor = documentation.getSlotDescriptor as jest.Mock
2222
mockGetSlotDescriptor.mockReturnValue(mockSlotDescriptor)
2323
})
@@ -55,4 +55,75 @@ describe('render function slotHandler', () => {
5555
}
5656
expect(documentation.getSlotDescriptor).toHaveBeenCalledWith('myScopedSlot')
5757
})
58+
59+
it('should find scoped slots in render object method', () => {
60+
const src = `
61+
export default {
62+
render(createElement) {
63+
return createElement('div', [
64+
this.$scopedSlots.myOtherScopedSlot({
65+
text: this.message
66+
})
67+
])
68+
}
69+
}
70+
`
71+
const def = parse(src)
72+
if (def) {
73+
slotHandler(documentation, def)
74+
}
75+
expect(documentation.getSlotDescriptor).toHaveBeenCalledWith('myOtherScopedSlot')
76+
})
77+
78+
it('should find slots in jsx render', () => {
79+
const src = `
80+
export default {
81+
render(createElement) {
82+
return (<div>,
83+
<slot name="myMain"/>
84+
</div>)
85+
}
86+
}
87+
`
88+
const def = parse(src)
89+
if (def) {
90+
slotHandler(documentation, def)
91+
}
92+
expect(documentation.getSlotDescriptor).toHaveBeenCalledWith('myMain')
93+
})
94+
95+
it('should find default slots in jsx render', () => {
96+
const src = `
97+
export default {
98+
render(createElement) {
99+
return (<div>,
100+
<slot/>
101+
</div>)
102+
}
103+
}
104+
`
105+
const def = parse(src)
106+
if (def) {
107+
slotHandler(documentation, def)
108+
}
109+
expect(documentation.getSlotDescriptor).toHaveBeenCalledWith('default')
110+
})
111+
112+
it('should allow describing slots in jsx render', () => {
113+
const src = `
114+
export default {
115+
render(createElement) {
116+
return (<div>,
117+
{/** @slot Use this slot header */}
118+
<slot/>
119+
</div>)
120+
}
121+
}
122+
`
123+
const def = parse(src)
124+
if (def) {
125+
slotHandler(documentation, def)
126+
}
127+
expect(mockSlotDescriptor.description).toEqual('Use this slot header')
128+
})
58129
})

src/script-handlers/slotHandler.ts

+65-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as bt from '@babel/types'
22
import { NodePath } from 'ast-types'
3-
import { Documentation, ParamTag, ParamType } from '../Documentation'
3+
import { Documentation, ParamTag, ParamType, Tag } from '../Documentation'
4+
import getDoclets from '../utils/getDoclets'
45

56
export interface TypedParamTag extends ParamTag {
67
type: ParamType
@@ -9,18 +10,24 @@ export interface TypedParamTag extends ParamTag {
910
// tslint:disable-next-line:no-var-requires
1011
import recast = require('recast')
1112

12-
export default function eventHandler(documentation: Documentation, path: NodePath) {
13+
export default function slotHandler(documentation: Documentation, path: NodePath) {
1314
if (bt.isObjectExpression(path.node)) {
1415
const renderPath = path
1516
.get('properties')
16-
.filter((p: NodePath) => bt.isObjectProperty(p.node) && p.node.key.name === 'render')
17+
.filter(
18+
(p: NodePath) =>
19+
(bt.isObjectProperty(p.node) || bt.isObjectMethod(p.node)) &&
20+
p.node.key.name === 'render',
21+
)
1722

1823
// if no prop return
1924
if (!renderPath.length) {
2025
return
2126
}
2227

23-
const renderValuePath = renderPath[0].get('value')
28+
const renderValuePath = bt.isObjectProperty(renderPath[0].node)
29+
? renderPath[0].get('value')
30+
: renderPath[0]
2431
recast.visit(renderValuePath, {
2532
visitCallExpression(pathCall: NodePath<bt.CallExpression>) {
2633
if (
@@ -48,6 +55,60 @@ export default function eventHandler(documentation: Documentation, path: NodePat
4855
}
4956
this.traverse(pathMember)
5057
},
58+
visitJSXElement(pathJSX: NodePath<bt.JSXElement>) {
59+
const tagName = pathJSX.node.openingElement.name
60+
if (bt.isJSXIdentifier(tagName) && tagName.name === 'slot') {
61+
const doc = documentation.getSlotDescriptor(getName(pathJSX))
62+
doc.description = getDescription(pathJSX)
63+
}
64+
this.traverse(pathJSX)
65+
},
5166
})
5267
}
5368
}
69+
70+
function getName(pathJSX: NodePath<bt.JSXElement>): string {
71+
const oe = pathJSX.node.openingElement
72+
const names = oe.attributes.filter(
73+
(a: bt.JSXAttribute) => bt.isJSXAttribute(a) && a.name.name === 'name',
74+
) as bt.JSXAttribute[]
75+
76+
const nameNode = names.length ? names[0].value : null
77+
return nameNode && bt.isStringLiteral(nameNode) ? nameNode.value : 'default'
78+
}
79+
80+
function getDescription(pathJSX: NodePath<bt.JSXElement>): string {
81+
const siblings = (pathJSX.parentPath.node as bt.JSXElement).children
82+
if (!siblings) {
83+
return ''
84+
}
85+
const indexInParent = siblings.indexOf(pathJSX.node)
86+
87+
let commentExpression: bt.JSXExpressionContainer | null = null
88+
for (let i = indexInParent - 1; i > -1; i--) {
89+
const currentNode = siblings[i]
90+
if (bt.isJSXExpressionContainer(currentNode)) {
91+
commentExpression = currentNode
92+
break
93+
}
94+
}
95+
if (!commentExpression || !commentExpression.expression.innerComments) {
96+
return ''
97+
}
98+
const cmts = commentExpression.expression.innerComments
99+
const lastComment = cmts[cmts.length - 1]
100+
if (lastComment.type !== 'CommentBlock') {
101+
return ''
102+
}
103+
const docBlock = lastComment.value.replace(/^\*/, '').trim()
104+
const jsDoc = getDoclets(docBlock)
105+
if (!jsDoc.tags) {
106+
return ''
107+
}
108+
const slotTags = jsDoc.tags.filter(t => t.title === 'slot')
109+
if (slotTags.length) {
110+
const tagContent = (slotTags[0] as Tag).content
111+
return typeof tagContent === 'string' ? tagContent : ''
112+
}
113+
return ''
114+
}

tests/components/grid-jsx/Grid.vue

+2
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ export default {
9595
const { sortKey, capitalize } = this
9696
return (
9797
<table class="grid">
98+
{/** @slot Use this slot header */}
99+
<slot name="header" />
98100
<thead>
99101
<tr>
100102
{columns.map(key => (

tests/components/grid-jsx/__snapshots__/grid-jsx.test.ts.snap

+5-1
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,11 @@ Object {
191191
},
192192
},
193193
},
194-
"slots": Object {},
194+
"slots": Object {
195+
"header": Object {
196+
"description": "Use this slot header",
197+
},
198+
},
195199
"tags": Object {
196200
"author": Array [
197201
Object {

tests/components/grid-jsx/grid-jsx.test.ts

+4
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,10 @@ describe('tests grid jsx', () => {
109109
expect(docGrid.methods[0].returns).toMatchObject({ description: 'Test' })
110110
})
111111

112+
it('should return slots from the render method', () => {
113+
expect(docGrid.slots.header).toMatchObject({ description: 'Use this slot header' })
114+
})
115+
112116
it('should match the snapshot', () => {
113117
expect(docGrid).toMatchSnapshot()
114118
})

0 commit comments

Comments
 (0)