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

feat: Display functional components in component tree #719

Merged
merged 9 commits into from
Aug 2, 2018
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
8 changes: 4 additions & 4 deletions cypress/integration/components-tab.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,25 +46,25 @@ suite('components tab', () => {

it('should expand child instance', () => {
cy.get('.instance .instance:nth-child(2) .arrow-wrapper').click()
cy.get('.instance').should('have.length', baseInstanceCount + 2)
cy.get('.instance').should('have.length', baseInstanceCount + 7)
})

it('should add/remove component from app side', () => {
cy.get('#target').iframe().then(({ get }) => {
get('.add').click({ force: true })
})
cy.get('.instance').should('have.length', baseInstanceCount + 5)
cy.get('.instance').should('have.length', baseInstanceCount + 10)
cy.get('#target').iframe().then(({ get }) => {
get('.remove').click({ force: true })
})
cy.get('.instance').should('have.length', baseInstanceCount + 4)
cy.get('.instance').should('have.length', baseInstanceCount + 9)
})

it('should filter components', () => {
cy.get('.left .search input').clear().type('counter')
cy.get('.instance').should('have.length', 1)
cy.get('.left .search input').clear().type('target')
cy.get('.instance').should('have.length', 5)
cy.get('.instance').should('have.length', 10)
cy.get('.left .search input').clear()
})

Expand Down
5 changes: 5 additions & 0 deletions shells/dev/target/Functional.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<template functional>
<div>
Hello {{ props.name }}
</div>
</template>
13 changes: 12 additions & 1 deletion shells/dev/target/Target.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,25 @@
>Inspect component</button>
<span v-if="over" class="over">Mouse over</span>
</div>
<div>
<Functional
v-for="n in 5"
:key="n"
:name="`Row ${n}`"
/>
</div>
</div>
</template>

<script>
import Other from './Other.vue'
import MyClass from './MyClass.js'
import Functional from './Functional.vue'
export default {
components: { Other },
components: {
Other,
Functional
},
props: {
msg: String,
obj: null,
Expand Down
15 changes: 8 additions & 7 deletions src/backend/highlighter.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { inDoc, classify } from '../util'
import { inDoc, classify, getComponentName } from '../util'
import { getInstanceName } from './index'
import SharedData from 'src/shared-data'

Expand Down Expand Up @@ -28,10 +28,10 @@ overlay.appendChild(overlayContent)

export function highlight (instance) {
if (!instance) return
const rect = getInstanceRect(instance)
const rect = getInstanceOrVnodeRect(instance)
if (rect) {
let content = ''
let name = getInstanceName(instance)
let name = instance.fnContext ? getComponentName(instance.fnOptions) : getInstanceName(instance)
if (SharedData.classifyComponents) name = classify(name)
if (name) content = `<span style="opacity: .6;">&lt;</span>${name}<span style="opacity: .6;">&gt;</span>`
showOverlay(rect, content)
Expand All @@ -55,14 +55,15 @@ export function unHighlight () {
* @return {Object}
*/

export function getInstanceRect (instance) {
if (!inDoc(instance.$el)) {
export function getInstanceOrVnodeRect (instance) {
const el = instance.$el || instance.elm
if (!inDoc(el)) {
return
}
if (instance._isFragment) {
return getFragmentRect(instance)
} else if (instance.$el.nodeType === 1) {
return instance.$el.getBoundingClientRect()
} else if (el.nodeType === 1) {
return el.getBoundingClientRect()
}
}

Expand Down
123 changes: 107 additions & 16 deletions src/backend/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// This is the backend that is injected into the page that a Vue app lives in
// when the Vue Devtools panel is activated.

import { highlight, unHighlight, getInstanceRect } from './highlighter'
import { highlight, unHighlight, getInstanceOrVnodeRect } from './highlighter'
import { initVuexBackend } from './vuex'
import { initEventsBackend } from './events'
import { findRelatedComponent } from './utils'
Expand All @@ -15,13 +15,15 @@ const rootInstances = []
const propModes = ['default', 'sync', 'once']

export const instanceMap = window.__VUE_DEVTOOLS_INSTANCE_MAP__ = new Map()
export const functionalVnodeMap = window.__VUE_DEVTOOLS_FUNCTIONAL_VNODE_MAP__ = new Map()
const consoleBoundInstances = Array(5)
let currentInspectedId
let bridge
let filter = ''
let captureCount = 0
let isLegacy = false
let rootUID = 0
let functionalIds = new Map()

export function initBackend (_bridge) {
bridge = _bridge
Expand Down Expand Up @@ -62,14 +64,14 @@ function connect (Vue) {

bridge.on('select-instance', id => {
currentInspectedId = id
const instance = instanceMap.get(id)
bindToConsole(instance)
const instance = findInstanceOrVnode(id)
if (!/:functional:/.test(id)) bindToConsole(instance)
flush()
bridge.send('instance-selected')
})

bridge.on('scroll-to-instance', id => {
const instance = instanceMap.get(id)
const instance = findInstanceOrVnode(id)
instance && scrollIntoView(instance)
})

Expand All @@ -80,7 +82,7 @@ function connect (Vue) {

bridge.on('refresh', scan)

bridge.on('enter-instance', id => highlight(instanceMap.get(id)))
bridge.on('enter-instance', id => highlight(findInstanceOrVnode(id)))

bridge.on('leave-instance', unHighlight)

Expand Down Expand Up @@ -145,6 +147,15 @@ function connect (Vue) {
setTimeout(scan, 0)
}

export function findInstanceOrVnode (id) {
if (/:functional:/.test(id)) {
const [refId] = id.split(':functional:')

return functionalVnodeMap.get(refId)[id]
}
return instanceMap.get(id)
}

/**
* Scan the page for root level Vue instances.
*/
Expand Down Expand Up @@ -225,6 +236,7 @@ function walk (node, fn) {

function flush () {
let start
functionalIds.clear()
if (process.env.NODE_ENV !== 'production') {
captureCount = 0
start = window.performance.now()
Expand Down Expand Up @@ -262,20 +274,27 @@ function findQualifiedChildrenFromList (instances) {
* If the instance itself is qualified, just return itself.
* This is ok because [].concat works in both cases.
*
* @param {Vue} instance
* @param {Vue|Vnode} instance
* @return {Vue|Array}
*/

function findQualifiedChildren (instance) {
return isQualified(instance)
? capture(instance)
: findQualifiedChildrenFromList(instance.$children)
: findQualifiedChildrenFromList(instance.$children).concat(
instance._vnode && instance._vnode.children
// Find functional components in recursively in non-functional vnodes.
? flatten(instance._vnode.children.filter(child => !child.componentInstance).map(captureChild))
// Filter qualified children.
.filter(({ name }) => name.indexOf(filter) > -1)
: []
)
}

/**
* Check if an instance is qualified.
*
* @param {Vue} instance
* @param {Vue|Vnode} instance
* @return {Boolean}
*/

Expand All @@ -284,17 +303,64 @@ function isQualified (instance) {
return name.indexOf(filter) > -1
}

function flatten (items) {
return items.reduce((acc, item) => {
if (item instanceof Array) acc.push(...flatten(item))
else if (item) acc.push(item)

return acc
}, [])
}

function captureChild (child) {
if (child.fnContext) {
return capture(child)
} else if (child.componentInstance) {
if (!child._isBeingDestroyed) return capture(child.componentInstance)
} else if (child.children) {
return flatten(child.children.map(captureChild))
}
}

/**
* Capture the meta information of an instance. (recursive)
*
* @param {Vue} instance
* @return {Object}
*/

function capture (instance, _, list) {
function capture (instance, index, list) {
if (process.env.NODE_ENV !== 'production') {
captureCount++
}

// Functional component.
if (instance.fnContext) {
const contextUid = instance.fnContext.__VUE_DEVTOOLS_UID__
let id = functionalIds.get(contextUid)
if (id == null) {
id = 0
} else {
id++
}
functionalIds.set(contextUid, id)
const functionalId = contextUid + ':functional:' + id
markFunctional(functionalId, instance)
return {
id: functionalId,
functional: true,
name: getComponentName(instance.fnOptions) || 'Anonymous Component',
renderKey: getRenderKey(instance.key),
children: instance.children ? instance.children.map(
child => child.fnContext
? capture(child)
: child.componentInstance
? capture(child.componentInstance)
: undefined).filter(Boolean) : [],
inactive: false, // TODO: Check what is it for.
isFragment: false // TODO: Check what is it for.
}
}
// instance._uid is not reliable in devtools as there
// may be 2 roots with same _uid which causes unexpected
// behaviour
Expand All @@ -306,13 +372,13 @@ function capture (instance, _, list) {
renderKey: getRenderKey(instance.$vnode ? instance.$vnode['key'] : null),
inactive: !!instance._inactive,
isFragment: !!instance._isFragment,
children: instance.$children
.filter(child => !child._isBeingDestroyed)
.map(capture)
children: instance._vnode.children
? flatten((instance._vnode.children).map(captureChild))
: instance.$children.filter(child => !child._isBeingDestroyed).map(capture)
}
// record screen position to ensure correct ordering
if ((!list || list.length > 1) && !instance._inactive) {
const rect = getInstanceRect(instance)
const rect = getInstanceOrVnodeRect(instance)
ret.top = rect ? rect.top : Infinity
} else {
ret.top = Infinity
Expand Down Expand Up @@ -353,6 +419,18 @@ function mark (instance) {
}
}

function markFunctional (id, vnode) {
const refId = vnode.fnContext.__VUE_DEVTOOLS_UID__
if (!functionalVnodeMap.has(refId)) {
functionalVnodeMap.set(refId, {})
vnode.fnContext.$on('hook:beforeDestroy', function () {
functionalVnodeMap.delete(refId)
})
}

functionalVnodeMap.get(refId)[id] = vnode
}

/**
* Get the detailed information of an inspected instance.
*
Expand All @@ -362,7 +440,19 @@ function mark (instance) {
function getInstanceDetails (id) {
const instance = instanceMap.get(id)
if (!instance) {
return {}
const vnode = findInstanceOrVnode(id)

if (!vnode) return {}

const data = {
id,
name: getComponentName(vnode.fnOptions),
file: vnode.fnOptions.__file || null,
state: processProps({ $options: vnode.fnOptions, ...(vnode.devtoolsMeta && vnode.devtoolsMeta.props) }),
functional: true
}

return data
} else {
const data = {
id: id,
Expand Down Expand Up @@ -427,7 +517,7 @@ export function reduceStateList (list) {
*/

export function getInstanceName (instance) {
const name = getComponentName(instance.$options)
const name = getComponentName(instance.$options || instance.fnOptions)
if (name) return name
return instance.$root === instance
? 'Root'
Expand Down Expand Up @@ -701,7 +791,7 @@ function processObservables (instance) {
*/

function scrollIntoView (instance) {
const rect = getInstanceRect(instance)
const rect = getInstanceOrVnodeRect(instance)
if (rect) {
window.scrollBy(0, rect.top + (rect.height - window.innerHeight) / 2)
}
Expand All @@ -715,6 +805,7 @@ function scrollIntoView (instance) {
*/

function bindToConsole (instance) {
if (!instance) return
const id = instance.__VUE_DEVTOOLS_UID__
const index = consoleBoundInstances.indexOf(id)
if (index > -1) {
Expand Down
Loading