Skip to content
This repository was archived by the owner on Mar 8, 2019. It is now read-only.
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
73 changes: 72 additions & 1 deletion src/script-handlers/__tests__/slotHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ describe('render function slotHandler', () => {

beforeEach(() => {
mockSlotDescriptor = { description: '' }
documentation = new (require('../../Documentation')).Documentation()
documentation = new Documentation()
const mockGetSlotDescriptor = documentation.getSlotDescriptor as jest.Mock
mockGetSlotDescriptor.mockReturnValue(mockSlotDescriptor)
})
Expand Down Expand Up @@ -55,4 +55,75 @@ describe('render function slotHandler', () => {
}
expect(documentation.getSlotDescriptor).toHaveBeenCalledWith('myScopedSlot')
})

it('should find scoped slots in render object method', () => {
const src = `
export default {
render(createElement) {
return createElement('div', [
this.$scopedSlots.myOtherScopedSlot({
text: this.message
})
])
}
}
`
const def = parse(src)
if (def) {
slotHandler(documentation, def)
}
expect(documentation.getSlotDescriptor).toHaveBeenCalledWith('myOtherScopedSlot')
})

it('should find slots in jsx render', () => {
const src = `
export default {
render(createElement) {
return (<div>,
<slot name="myMain"/>
</div>)
}
}
`
const def = parse(src)
if (def) {
slotHandler(documentation, def)
}
expect(documentation.getSlotDescriptor).toHaveBeenCalledWith('myMain')
})

it('should find default slots in jsx render', () => {
const src = `
export default {
render(createElement) {
return (<div>,
<slot/>
</div>)
}
}
`
const def = parse(src)
if (def) {
slotHandler(documentation, def)
}
expect(documentation.getSlotDescriptor).toHaveBeenCalledWith('default')
})

it('should allow describing slots in jsx render', () => {
const src = `
export default {
render(createElement) {
return (<div>,
{/** @slot Use this slot header */}
<slot/>
</div>)
}
}
`
const def = parse(src)
if (def) {
slotHandler(documentation, def)
}
expect(mockSlotDescriptor.description).toEqual('Use this slot header')
})
})
69 changes: 65 additions & 4 deletions src/script-handlers/slotHandler.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as bt from '@babel/types'
import { NodePath } from 'ast-types'
import { Documentation, ParamTag, ParamType } from '../Documentation'
import { Documentation, ParamTag, ParamType, Tag } from '../Documentation'
import getDoclets from '../utils/getDoclets'

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

export default function eventHandler(documentation: Documentation, path: NodePath) {
export default function slotHandler(documentation: Documentation, path: NodePath) {
if (bt.isObjectExpression(path.node)) {
const renderPath = path
.get('properties')
.filter((p: NodePath) => bt.isObjectProperty(p.node) && p.node.key.name === 'render')
.filter(
(p: NodePath) =>
(bt.isObjectProperty(p.node) || bt.isObjectMethod(p.node)) &&
p.node.key.name === 'render',
)

// if no prop return
if (!renderPath.length) {
return
}

const renderValuePath = renderPath[0].get('value')
const renderValuePath = bt.isObjectProperty(renderPath[0].node)
? renderPath[0].get('value')
: renderPath[0]
recast.visit(renderValuePath, {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you can return JSX from both methods and computed properties (and maybe even data?), I think we should visit the full Vue object here.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'll make the PR bigger though since we need to add all the relevant tests

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that is a good idea. In this case, you should create another handler so that not to polute the render function handler already present.

visitCallExpression(pathCall: NodePath<bt.CallExpression>) {
if (
Expand Down Expand Up @@ -48,6 +55,60 @@ export default function eventHandler(documentation: Documentation, path: NodePat
}
this.traverse(pathMember)
},
visitJSXElement(pathJSX: NodePath<bt.JSXElement>) {
const tagName = pathJSX.node.openingElement.name
if (bt.isJSXIdentifier(tagName) && tagName.name === 'slot') {
const doc = documentation.getSlotDescriptor(getName(pathJSX))
doc.description = getDescription(pathJSX)
}
this.traverse(pathJSX)
},
})
}
}

function getName(pathJSX: NodePath<bt.JSXElement>): string {
const oe = pathJSX.node.openingElement
const names = oe.attributes.filter(
(a: bt.JSXAttribute) => bt.isJSXAttribute(a) && a.name.name === 'name',
) as bt.JSXAttribute[]

const nameNode = names.length ? names[0].value : null
return nameNode && bt.isStringLiteral(nameNode) ? nameNode.value : 'default'
}

function getDescription(pathJSX: NodePath<bt.JSXElement>): string {
const siblings = (pathJSX.parentPath.node as bt.JSXElement).children
if (!siblings) {
return ''
}
const indexInParent = siblings.indexOf(pathJSX.node)

let commentExpression: bt.JSXExpressionContainer | null = null
for (let i = indexInParent - 1; i > -1; i--) {
const currentNode = siblings[i]
if (bt.isJSXExpressionContainer(currentNode)) {
commentExpression = currentNode
break
}
}
if (!commentExpression || !commentExpression.expression.innerComments) {
return ''
}
const cmts = commentExpression.expression.innerComments
const lastComment = cmts[cmts.length - 1]
if (lastComment.type !== 'CommentBlock') {
return ''
}
const docBlock = lastComment.value.replace(/^\*/, '').trim()
const jsDoc = getDoclets(docBlock)
if (!jsDoc.tags) {
return ''
}
const slotTags = jsDoc.tags.filter(t => t.title === 'slot')
if (slotTags.length) {
const tagContent = (slotTags[0] as Tag).content
return typeof tagContent === 'string' ? tagContent : ''
}
return ''
}
2 changes: 2 additions & 0 deletions tests/components/grid-jsx/Grid.vue
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ export default {
const { sortKey, capitalize } = this
return (
<table class="grid">
{/** @slot Use this slot header */}
<slot name="header" />
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps also add a method/computed property with the footer slot, and then include it below. See my comment above

<thead>
<tr>
{columns.map(key => (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,11 @@ Object {
},
},
},
"slots": Object {},
"slots": Object {
"header": Object {
"description": "Use this slot header",
},
},
"tags": Object {
"author": Array [
Object {
Expand Down
4 changes: 4 additions & 0 deletions tests/components/grid-jsx/grid-jsx.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ describe('tests grid jsx', () => {
expect(docGrid.methods[0].returns).toMatchObject({ description: 'Test' })
})

it('should return slots from the render method', () => {
expect(docGrid.slots.header).toMatchObject({ description: 'Use this slot header' })
})

it('should match the snapshot', () => {
expect(docGrid).toMatchSnapshot()
})
Expand Down