Skip to content

Support native Date type, Components, improved RegExp, new CustomValue API #474

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

Merged
merged 6 commits into from
Jan 12, 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
55 changes: 55 additions & 0 deletions shells/dev/target/NativeTypes.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<template>
<div id="date">
<p>Date: {{ date.toString() }} - Hours: {{ hours }} - Prototype: {{ date | prototypeString }}</p>

<p>
<button @click="updateDate">Update Date</button>
</p>

<hr>

<TestComponent ref="component" />

<p>
<button @click="sendComponent">Vuex mutation</button>
</p>
</div>
</template>

<script>
import { mapState, mapGetters, mapMutations } from 'vuex'

export default {
components: {
TestComponent: {
data: () => ({ foo: '42' }),
props: { bar: { default: 'hey' }},
render: h => h('div', '<TestComponent />')
}
},
data () {
return {
localDate: new Date(),
testComponent: null
}
},
computed: {
...mapState(['date']),
...mapGetters(['hours'])
},
mounted () {
this.testComponent = this.$refs.component
},
methods: {
...mapMutations({
updateDate: 'UPDATE_DATE'
}),
sendComponent () {
this.$store.commit('TEST_COMPONENT', this.testComponent)
}
},
filters: {
prototypeString: val => Object.prototype.toString.call(val)
}
}
</script>
4 changes: 3 additions & 1 deletion shells/dev/target/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import store from './store'
import Target from './Target.vue'
import Other from './Other.vue'
import Counter from './Counter.vue'
import NativeTypes from './NativeTypes.vue'
import Events from './Events.vue'
import MyClass from './MyClass.js'

Expand All @@ -21,7 +22,8 @@ new Vue({
h(Counter),
h(Target, {props:{msg: 'hi', ins: new MyClass()}}),
h(Other),
h(Events)
h(Events),
h(NativeTypes)
])
},
data: {
Expand Down
12 changes: 9 additions & 3 deletions shells/dev/target/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,19 @@ Vue.use(Vuex)

export default new Vuex.Store({
state: {
count: 0
count: 0,
date: new Date()
},
mutations: {
INCREMENT: state => state.count++,
DECREMENT: state => state.count--
DECREMENT: state => state.count--,
UPDATE_DATE: state => {
state.date = new Date()
},
TEST_COMPONENT: state => {}
},
getters: {
isPositive: state => state.count >= 0
isPositive: state => state.count >= 0,
hours: state => state.date.getHours()
}
})
46 changes: 37 additions & 9 deletions src/backend/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const hook = window.__VUE_DEVTOOLS_GLOBAL_HOOK__
const rootInstances = []
const propModes = ['default', 'sync', 'once']

const instanceMap = window.__VUE_DEVTOOLS_INSTANCE_MAP__ = new Map()
export const instanceMap = window.__VUE_DEVTOOLS_INSTANCE_MAP__ = new Map()
const consoleBoundInstances = Array(5)
let currentInspectedId
let bridge
Expand Down Expand Up @@ -320,18 +320,46 @@ function getInstanceDetails (id) {
return {
id: id,
name: getInstanceName(instance),
state: processProps(instance).concat(
processState(instance),
processComputed(instance),
processRouteContext(instance),
processVuexGetters(instance),
processFirebaseBindings(instance),
processObservables(instance)
)
state: getInstanceState(instance)
}
}
}

function getInstanceState (instance) {
return processProps(instance).concat(
processState(instance),
processComputed(instance),
processRouteContext(instance),
processVuexGetters(instance),
processFirebaseBindings(instance),
processObservables(instance)
)
}

export function getCustomInstanceDetails (instance) {
const state = getInstanceState(instance)
return {
_custom: {
type: 'component',
id: instance.__VUE_DEVTOOLS_UID__,
display: getInstanceName(instance),
state: reduceStateList(state)
}
}
}

export function reduceStateList (list) {
if (!list.length) {
Copy link
Member

Choose a reason for hiding this comment

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

minor, but you can write this as if (!list.length) return

return undefined
}
return list.reduce((map, item) => {
const key = item.type || 'data'
const obj = map[key] = map[key] || {}
obj[item.key] = item.value
return map
}, {})
}

/**
* Get the appropriate display name for an instance.
*
Expand Down
55 changes: 45 additions & 10 deletions src/devtools/components/DataField.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@
:class="{ rotated: expanded }"
v-show="isExpandableType">
</span>
<span class="key">{{ field.key }}</span>
<span class="colon">:<div class="meta" v-if="field.meta">
<span class="key" :class="{ special: field.noDisplay }">{{ field.key }}</span>
<span class="colon"><span v-if="!field.noDisplay">:</span><div class="meta" v-if="field.meta">
<div class="meta-field" v-for="(val, key) in field.meta">
<span class="key">{{ key }}</span>
<span class="value">{{ val }}</span>
</div>
</div></span>
<span class="value" :class="valueType">{{ formattedValue }}</span>
<span class="value" :class="valueClass">{{ formattedValue }}</span>
</div>
<div class="children" v-if="expanded && isExpandableType">
<data-field
Expand Down Expand Up @@ -44,7 +44,7 @@ import {
} from 'src/util'

const rawTypeRE = /^\[object (\w+)]$/
const specialTypeRE = /^\[native \w+ (.*)\]$/
const specialTypeRE = /^\[native (\w+) (.*)\]$/

function subFieldCount (value) {
if (Array.isArray(value)) {
Expand Down Expand Up @@ -81,32 +81,41 @@ export default {
value === NAN
) {
return 'literal'
} else if (value && value._custom) {
return 'custom'
} else if (specialTypeRE.test(value)) {
return 'native'
const [, type] = specialTypeRE.exec(value)
return `native ${type}`
} else if (type === 'string' && !rawTypeRE.test(value)) {
return 'string'
}
},
isExpandableType () {
const value = this.field.value
return Array.isArray(value) || isPlainObject(value)
return Array.isArray(value) ||
(this.valueType === 'custom' && value._custom.state) ||
(this.valueType !== 'custom' && isPlainObject(value))
},
formattedValue () {
const value = this.field.value
if (value === null) {
if (this.field.noDisplay) {
return ''
} else if (value === null) {
return 'null'
} else if (value === UNDEFINED) {
return 'undefined'
} else if (value === NAN) {
return 'NaN'
} else if (value === INFINITY) {
return 'Infinity'
} else if (this.valueType === 'custom') {
return value._custom.display
} else if (Array.isArray(value)) {
return 'Array[' + value.length + ']'
} else if (isPlainObject(value)) {
return 'Object' + (Object.keys(value).length ? '' : ' (empty)')
} else if (this.valueType === 'native') {
return specialTypeRE.exec(value)[1]
} else if (this.valueType.includes('native')) {
return specialTypeRE.exec(value)[2]
} else if (typeof value === 'string') {
var typeMatch = value.match(rawTypeRE)
if (typeMatch) {
Expand All @@ -126,15 +135,29 @@ export default {
value: item
}))
} else if (typeof value === 'object') {
const isCustom = this.valueType === 'custom'
if (isCustom) {
value = value._custom.state
}
value = sortByKey(Object.keys(value).map(key => ({
key,
value: value[key]
value: value[key],
noDisplay: isCustom
})))
}
return value
},
limitedSubFields () {
return this.formattedSubFields.slice(0, this.limit)
},
valueClass () {
const cssClass = [this.valueType]
if (this.valueType === 'custom') {
const value = this.field.value
value._custom.type && cssClass.push(`type-${value._custom.type}`)
value._custom.class && cssClass.push(value._custom.class)
}
return cssClass
}
},
methods: {
Expand Down Expand Up @@ -175,6 +198,8 @@ export default {
transform rotate(90deg)
.key
color #881391
&.special
color $blueishGrey
.colon
margin-right .5em
position relative
Expand All @@ -186,6 +211,16 @@ export default {
color #999
&.literal
color #0033cc
&.custom
&.type-component
color $green
&::before,
&::after
color $darkerGrey
&::before
content '<'
&::after
content '>'

.type
color $background-color
Expand Down
2 changes: 2 additions & 0 deletions src/devtools/variables.styl
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Colors
$blue = #44A1FF
$grey = #DDDDDD
$darkerGrey = #CCC
$blueishGrey = #486887
$green = #42B983
$darkerGreen = #3BA776
$slate = #242424
Expand Down
2 changes: 1 addition & 1 deletion src/devtools/views/components/ComponentInstance.vue
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ export default {
transform rotate(90deg)

.angle-bracket
color #ccc
color $darkerGrey

.instance-name
color $component-color
Expand Down
18 changes: 17 additions & 1 deletion src/util.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import CircularJSON from 'circular-json-es6'

import { instanceMap, getCustomInstanceDetails } from 'src/backend'

function cached (fn) {
const cache = Object.create(null)
return function cachedFn (str) {
Expand Down Expand Up @@ -43,7 +45,8 @@ export function stringify (data) {
return CircularJSON.stringify(data, replacer)
}

function replacer (key, val) {
function replacer (key) {
const val = this[key]
Copy link
Member Author

Choose a reason for hiding this comment

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

@yyx990803 This is to prevent the default replacer to turn Dates into Strings before calling the custom replacer. See WebReflection/circular-json#4

if (val === undefined) {
return UNDEFINED
} else if (val === Infinity) {
Expand All @@ -53,6 +56,10 @@ function replacer (key, val) {
} else if (val instanceof RegExp) {
// special handling of native type
return `[native RegExp ${val.toString()}]`
} else if (val instanceof Date) {
return `[native Date ${val.toString()}]`
} else if (val && val._isVue) {
return getCustomInstanceDetails(val)
} else {
return sanitize(val)
}
Expand All @@ -64,13 +71,22 @@ export function parse (data, revive) {
: CircularJSON.parse(data)
}

const specialTypeRE = /^\[native (\w+) (.*)\]$/

function reviver (key, val) {
if (val === UNDEFINED) {
return undefined
} else if (val === INFINITY) {
return Infinity
} else if (val === NAN) {
return NaN
} else if (val && val._custom) {
if (val._custom.type === 'component') {
return instanceMap.get(val._custom.id)
}
} else if (specialTypeRE.test(val)) {
const [, type, string] = specialTypeRE.exec(val)
return new window[type](string)
} else {
return val
}
Expand Down
7 changes: 4 additions & 3 deletions test/specs/test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module.exports = {
'vue-devtools e2e tests': function (browser) {
var baseInstanceCount = 6
var baseInstanceCount = 7

browser
.url('http://localhost:' + (process.env.PORT || 8081))
Expand Down Expand Up @@ -199,9 +199,10 @@ module.exports = {
.setValue('.import-state textarea', '{invalid: json}')
.waitForElementVisible('.message.invalid-json', 100)
.clearValue('.import-state textarea')
.setValue('.import-state textarea', '{"valid": "json"}')
.setValue('.import-state textarea', '{"count":42,"date":"[native Date Fri Dec 22 2017 10:12:04 GMT+0100 (CET)]"}')
.waitForElementNotVisible('.message.invalid-json', 1000)
.assert.containsText('.vuex-state-inspector', 'valid:"json"')
.assert.containsText('.vuex-state-inspector', 'count:42')
.assert.containsText('.vuex-state-inspector', 'date:Fri Dec 22 2017 10:12:04 GMT+0100 (CET)')
.click('.import')
.waitForElementNotPresent('.import-state', 2000)

Expand Down