Skip to content

Commit f310b0f

Browse files
znckAkryum
authored andcommitted
feat: Display functional components in component tree (#719)
* feat: Display functional components in component tree * feat: Functional component inspector * feat: Process props * chore: functional components example * fix: Recursively find component in vnodes * fix: select functional component + style * test: Update tests for functional components in tree * fix: selected style
1 parent c79dacd commit f310b0f

File tree

7 files changed

+166
-36
lines changed

7 files changed

+166
-36
lines changed

cypress/integration/components-tab.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,25 +46,25 @@ suite('components tab', () => {
4646

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

5252
it('should add/remove component from app side', () => {
5353
cy.get('#target').iframe().then(({ get }) => {
5454
get('.add').click({ force: true })
5555
})
56-
cy.get('.instance').should('have.length', baseInstanceCount + 5)
56+
cy.get('.instance').should('have.length', baseInstanceCount + 10)
5757
cy.get('#target').iframe().then(({ get }) => {
5858
get('.remove').click({ force: true })
5959
})
60-
cy.get('.instance').should('have.length', baseInstanceCount + 4)
60+
cy.get('.instance').should('have.length', baseInstanceCount + 9)
6161
})
6262

6363
it('should filter components', () => {
6464
cy.get('.left .search input').clear().type('counter')
6565
cy.get('.instance').should('have.length', 1)
6666
cy.get('.left .search input').clear().type('target')
67-
cy.get('.instance').should('have.length', 5)
67+
cy.get('.instance').should('have.length', 10)
6868
cy.get('.left .search input').clear()
6969
})
7070

shells/dev/target/Functional.vue

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<template functional>
2+
<div>
3+
Hello {{ props.name }}
4+
</div>
5+
</template>

shells/dev/target/Target.vue

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,25 @@
1818
>Inspect component</button>
1919
<span v-if="over" class="over">Mouse over</span>
2020
</div>
21+
<div>
22+
<Functional
23+
v-for="n in 5"
24+
:key="n"
25+
:name="`Row ${n}`"
26+
/>
27+
</div>
2128
</div>
2229
</template>
2330

2431
<script>
2532
import Other from './Other.vue'
2633
import MyClass from './MyClass.js'
34+
import Functional from './Functional.vue'
2735
export default {
28-
components: { Other },
36+
components: {
37+
Other,
38+
Functional
39+
},
2940
props: {
3041
msg: String,
3142
obj: null,

src/backend/highlighter.js

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { inDoc, classify } from '../util'
1+
import { inDoc, classify, getComponentName } from '../util'
22
import { getInstanceName } from './index'
33
import SharedData from 'src/shared-data'
44

@@ -28,10 +28,10 @@ overlay.appendChild(overlayContent)
2828

2929
export function highlight (instance) {
3030
if (!instance) return
31-
const rect = getInstanceRect(instance)
31+
const rect = getInstanceOrVnodeRect(instance)
3232
if (rect) {
3333
let content = ''
34-
let name = getInstanceName(instance)
34+
let name = instance.fnContext ? getComponentName(instance.fnOptions) : getInstanceName(instance)
3535
if (SharedData.classifyComponents) name = classify(name)
3636
if (name) content = `<span style="opacity: .6;">&lt;</span>${name}<span style="opacity: .6;">&gt;</span>`
3737
showOverlay(rect, content)
@@ -55,14 +55,15 @@ export function unHighlight () {
5555
* @return {Object}
5656
*/
5757

58-
export function getInstanceRect (instance) {
59-
if (!inDoc(instance.$el)) {
58+
export function getInstanceOrVnodeRect (instance) {
59+
const el = instance.$el || instance.elm
60+
if (!inDoc(el)) {
6061
return
6162
}
6263
if (instance._isFragment) {
6364
return getFragmentRect(instance)
64-
} else if (instance.$el.nodeType === 1) {
65-
return instance.$el.getBoundingClientRect()
65+
} else if (el.nodeType === 1) {
66+
return el.getBoundingClientRect()
6667
}
6768
}
6869

src/backend/index.js

Lines changed: 107 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// This is the backend that is injected into the page that a Vue app lives in
22
// when the Vue Devtools panel is activated.
33

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

1717
export const instanceMap = window.__VUE_DEVTOOLS_INSTANCE_MAP__ = new Map()
18+
export const functionalVnodeMap = window.__VUE_DEVTOOLS_FUNCTIONAL_VNODE_MAP__ = new Map()
1819
const consoleBoundInstances = Array(5)
1920
let currentInspectedId
2021
let bridge
2122
let filter = ''
2223
let captureCount = 0
2324
let isLegacy = false
2425
let rootUID = 0
26+
let functionalIds = new Map()
2527

2628
export function initBackend (_bridge) {
2729
bridge = _bridge
@@ -62,14 +64,14 @@ function connect (Vue) {
6264

6365
bridge.on('select-instance', id => {
6466
currentInspectedId = id
65-
const instance = instanceMap.get(id)
66-
bindToConsole(instance)
67+
const instance = findInstanceOrVnode(id)
68+
if (!/:functional:/.test(id)) bindToConsole(instance)
6769
flush()
6870
bridge.send('instance-selected')
6971
})
7072

7173
bridge.on('scroll-to-instance', id => {
72-
const instance = instanceMap.get(id)
74+
const instance = findInstanceOrVnode(id)
7375
instance && scrollIntoView(instance)
7476
})
7577

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

8183
bridge.on('refresh', scan)
8284

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

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

@@ -145,6 +147,15 @@ function connect (Vue) {
145147
setTimeout(scan, 0)
146148
}
147149

150+
export function findInstanceOrVnode (id) {
151+
if (/:functional:/.test(id)) {
152+
const [refId] = id.split(':functional:')
153+
154+
return functionalVnodeMap.get(refId)[id]
155+
}
156+
return instanceMap.get(id)
157+
}
158+
148159
/**
149160
* Scan the page for root level Vue instances.
150161
*/
@@ -225,6 +236,7 @@ function walk (node, fn) {
225236

226237
function flush () {
227238
let start
239+
functionalIds.clear()
228240
if (process.env.NODE_ENV !== 'production') {
229241
captureCount = 0
230242
start = window.performance.now()
@@ -262,20 +274,27 @@ function findQualifiedChildrenFromList (instances) {
262274
* If the instance itself is qualified, just return itself.
263275
* This is ok because [].concat works in both cases.
264276
*
265-
* @param {Vue} instance
277+
* @param {Vue|Vnode} instance
266278
* @return {Vue|Array}
267279
*/
268280

269281
function findQualifiedChildren (instance) {
270282
return isQualified(instance)
271283
? capture(instance)
272-
: findQualifiedChildrenFromList(instance.$children)
284+
: findQualifiedChildrenFromList(instance.$children).concat(
285+
instance._vnode && instance._vnode.children
286+
// Find functional components in recursively in non-functional vnodes.
287+
? flatten(instance._vnode.children.filter(child => !child.componentInstance).map(captureChild))
288+
// Filter qualified children.
289+
.filter(({ name }) => name.indexOf(filter) > -1)
290+
: []
291+
)
273292
}
274293

275294
/**
276295
* Check if an instance is qualified.
277296
*
278-
* @param {Vue} instance
297+
* @param {Vue|Vnode} instance
279298
* @return {Boolean}
280299
*/
281300

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

306+
function flatten (items) {
307+
return items.reduce((acc, item) => {
308+
if (item instanceof Array) acc.push(...flatten(item))
309+
else if (item) acc.push(item)
310+
311+
return acc
312+
}, [])
313+
}
314+
315+
function captureChild (child) {
316+
if (child.fnContext) {
317+
return capture(child)
318+
} else if (child.componentInstance) {
319+
if (!child._isBeingDestroyed) return capture(child.componentInstance)
320+
} else if (child.children) {
321+
return flatten(child.children.map(captureChild))
322+
}
323+
}
324+
287325
/**
288326
* Capture the meta information of an instance. (recursive)
289327
*
290328
* @param {Vue} instance
291329
* @return {Object}
292330
*/
293331

294-
function capture (instance, _, list) {
332+
function capture (instance, index, list) {
295333
if (process.env.NODE_ENV !== 'production') {
296334
captureCount++
297335
}
336+
337+
// Functional component.
338+
if (instance.fnContext) {
339+
const contextUid = instance.fnContext.__VUE_DEVTOOLS_UID__
340+
let id = functionalIds.get(contextUid)
341+
if (id == null) {
342+
id = 0
343+
} else {
344+
id++
345+
}
346+
functionalIds.set(contextUid, id)
347+
const functionalId = contextUid + ':functional:' + id
348+
markFunctional(functionalId, instance)
349+
return {
350+
id: functionalId,
351+
functional: true,
352+
name: getComponentName(instance.fnOptions) || 'Anonymous Component',
353+
renderKey: getRenderKey(instance.key),
354+
children: instance.children ? instance.children.map(
355+
child => child.fnContext
356+
? capture(child)
357+
: child.componentInstance
358+
? capture(child.componentInstance)
359+
: undefined).filter(Boolean) : [],
360+
inactive: false, // TODO: Check what is it for.
361+
isFragment: false // TODO: Check what is it for.
362+
}
363+
}
298364
// instance._uid is not reliable in devtools as there
299365
// may be 2 roots with same _uid which causes unexpected
300366
// behaviour
@@ -306,13 +372,13 @@ function capture (instance, _, list) {
306372
renderKey: getRenderKey(instance.$vnode ? instance.$vnode['key'] : null),
307373
inactive: !!instance._inactive,
308374
isFragment: !!instance._isFragment,
309-
children: instance.$children
310-
.filter(child => !child._isBeingDestroyed)
311-
.map(capture)
375+
children: instance._vnode.children
376+
? flatten((instance._vnode.children).map(captureChild))
377+
: instance.$children.filter(child => !child._isBeingDestroyed).map(capture)
312378
}
313379
// record screen position to ensure correct ordering
314380
if ((!list || list.length > 1) && !instance._inactive) {
315-
const rect = getInstanceRect(instance)
381+
const rect = getInstanceOrVnodeRect(instance)
316382
ret.top = rect ? rect.top : Infinity
317383
} else {
318384
ret.top = Infinity
@@ -353,6 +419,18 @@ function mark (instance) {
353419
}
354420
}
355421

422+
function markFunctional (id, vnode) {
423+
const refId = vnode.fnContext.__VUE_DEVTOOLS_UID__
424+
if (!functionalVnodeMap.has(refId)) {
425+
functionalVnodeMap.set(refId, {})
426+
vnode.fnContext.$on('hook:beforeDestroy', function () {
427+
functionalVnodeMap.delete(refId)
428+
})
429+
}
430+
431+
functionalVnodeMap.get(refId)[id] = vnode
432+
}
433+
356434
/**
357435
* Get the detailed information of an inspected instance.
358436
*
@@ -362,7 +440,19 @@ function mark (instance) {
362440
function getInstanceDetails (id) {
363441
const instance = instanceMap.get(id)
364442
if (!instance) {
365-
return {}
443+
const vnode = findInstanceOrVnode(id)
444+
445+
if (!vnode) return {}
446+
447+
const data = {
448+
id,
449+
name: getComponentName(vnode.fnOptions),
450+
file: vnode.fnOptions.__file || null,
451+
state: processProps({ $options: vnode.fnOptions, ...(vnode.devtoolsMeta && vnode.devtoolsMeta.props) }),
452+
functional: true
453+
}
454+
455+
return data
366456
} else {
367457
const data = {
368458
id: id,
@@ -427,7 +517,7 @@ export function reduceStateList (list) {
427517
*/
428518

429519
export function getInstanceName (instance) {
430-
const name = getComponentName(instance.$options)
520+
const name = getComponentName(instance.$options || instance.fnOptions)
431521
if (name) return name
432522
return instance.$root === instance
433523
? 'Root'
@@ -701,7 +791,7 @@ function processObservables (instance) {
701791
*/
702792

703793
function scrollIntoView (instance) {
704-
const rect = getInstanceRect(instance)
794+
const rect = getInstanceOrVnodeRect(instance)
705795
if (rect) {
706796
window.scrollBy(0, rect.top + (rect.height - window.innerHeight) / 2)
707797
}
@@ -715,6 +805,7 @@ function scrollIntoView (instance) {
715805
*/
716806

717807
function bindToConsole (instance) {
808+
if (!instance) return
718809
const id = instance.__VUE_DEVTOOLS_UID__
719810
const index = consoleBoundInstances.indexOf(id)
720811
if (index > -1) {

0 commit comments

Comments
 (0)