Vue.js object proxies: props not showing in Object.keys - javascript

Vue.js proxies its objects to catch property accesses. I seem to have found a leak in the abstraction: Object.keys doesn't return props in the list of keys.
With the following Vue component:
function callMe() {
var comp = Vue.component("comp", {
template: "<button #click='clickMe()'>xxx</button>",
props: {
label: String,
cardId: String,
collapsible: {
type: Boolean,
default: true,
},
collapsed: Boolean,
},
data() {
console.log(Object.keys(this))
console.log(this.collapsible)
console.log(Object.keys(this).includes("collapsible"))
return { isCollapsed: this.collapsed }
},
methods: {
clickMe(){
console.log(this)
}
}
})
var vm = new Vue({
el: '#root',
template: "<comp></comp>",
})
}
callMe();
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.11/dist/vue.js"></script>
<div id='root'>
<button #click="clickMe()" >Click Me</button>
</div>
The console output is:
(29) ["_uid", "_isVue", "$options", "_renderProxy", "_self", "$parent", "$root", "$children", "$refs", "_watcher", "_inactive", "_directInactive", "_isMounted", "_isDestroyed", "_isBeingDestroyed", "_events", "_hasHookEvent", "_vnode", "_staticTrees", "$vnode", "$slots", "$scopedSlots", "_c", "$createElement", "$attrs", "$listeners", "_watchers", "_props", "toggleThis"]
true
false
(Interestingly, when I call the check later, the isCollapsed item is in the list. You'll also notice that clickMe method is also present. It seems that only props are left out.)
Why is this happening?
More generally, how does Vue's Proxy object emit a different set of keys than it can then access?
This is a problem for me because I'm trying something fancy with pug-vdom and that internally uses Object.keys to enumerate the variables to inject into the Pug template.
Is this a Vue bug?
Alternately, is it possible to access a list of props keys from the this object, and export an object whose keys contain the props as well?
edit: added a runnable code snippet that demonstrates the problem.

Object.keys() does not iterate over prototype properties.
Also child component is inherited from root component.
This means props & data fields must be inside __proto__ of child components.
Hence if we do Object.keys(this__proto__).includes("collapsible") , it returns true in child component.
If you want to access those fields from child components then use this.$props and this.$data .
function callMe() {
var comp = Vue.component("comp", {
template: "<button #click='clickMe()'>xxx</button>",
props: {
label: String,
cardId: String,
collapsible: {
type: Boolean,
default: true,
},
collapsed: Boolean,
},
data() {
console.log('Inside Child:',Object.keys(this))
console.log(this.collapsible)
console.log(Object.keys(this.__proto__).includes("collapsible"))
console.log(Object.keys(this).includes("collapsible"))
return { isCollapsed: this.collapsed }
},
methods: {
clickMe(){
console.log(this)
}
}
})
var vm = new Vue({
el: '#root',
template: "<comp></comp>",
props:{
jack:{
type: Boolean,
default: true
}
},
data(){
console.log('Inside parent:', this.jack)
return {}
}
})
}
callMe();
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.11/dist/vue.js"></script>
<div id='root'>
<button #click="clickMe()" >Click Me</button>
</div>

Related

Vue: default value for prop function

Is there any way to set default function for prop with function type?
props: {
clickFunction: {
type: 'Function'
default: ????
}
}
Yes:
Vue.component('foo', {
template: '<div>{{ num }}</div>',
props: {
func: {
type: Function,
default: () => 1,
},
},
data() {
return {
num: this.func()
}
}
})
new Vue({
el: '#app',
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<foo></foo>
</div>
I'd say, always provide a default value that matched the type you specified: makes things much more easier to reason about. Here, what you are looking for would be a noop function (a.k.a. a function that does nothing):
props: {
clickFunction: {
type: Function
default: () => {}
}
}
You can then use this.clickFunction() in your code without checking it defensively first: it is always a function. Be sure you don't get mixed up with the default value for an Object prop, that would be an empty object:
props: {
someObject: {
type: Object
default: () => ({})
}
}
In Vue, Object and Array props must have their default value returned by a function to ensure they are distinct for every instance of the component (which does not seem to be required for Function props, but docs indeed aren't crystal clear about this).
This way should work with Vue 2.0
props: {
clickFunction: {
type: 'Function'
default(){
return () => true
}
}
}
I think this is not the right approach. VUE do not work that way. Why do want to pass reference to function as props? Component can emit its own event that parent can catch.
Here you have available props types:
https://v2.vuejs.org/v2/guide/components-props.html
And here you can read about cutom events:
https://v2.vuejs.org/v2/guide/components-custom-events.html#Event-Names
Edit
Yeah, my answer is not correct and does not answer the question.
Straight answer: yes, there are some any ways to set default function for prop with function type (look other answers).
Patterns and use-cases are off-topic so I end here (however, someone mentioned an interesting thing in response to my answer).

Reactivity in depth in data

I need some help. I have a component where I pass a prop and I need to assign a variable from my data to the prop variable, but in a reactive way. I cant modify the child it only accepts Booleans. The problem is that Vue initializates the data, but the disabled attribute isnt reactive. I know that if I pass an object to the disabled attribute that will be reactive, but I cant.
data() {
let editmode = true;
return {
EditMode: editmode,
schema: [
{
disabled: !editmode,
}
]
}
}
In the future I need to edit the value of EditMode and I want to that edit to be passed to my child component. I pass the schema variable to the child.
The attribute disabled is not reactive because he is not receiving a referenced variable to pass it reference across the data structure, basically you is saying disabled: !true, that evaluated to disabled: false, and "false" is just false, not a reference from the variable because a boolean is not referenceable, a object is (by example, and this is one of reasons to data hook return a object)! And if you change the editmode variable nothing is gona to happen ... Rethink your data structure and edit what your need putting these data in a object to be reactive! Hope it helps.
If you really need two separated variables, a workaround is use a watcher to detect when editMode changes and populate the changed value to another variable in data..
<template>
<div>
<button #click="change">Change Edit Mode {{editMode}}</button>
<child-component :isEditing="!schema.disabled"></child-component>
</div>
</template>
<script>
export default {
name: "test2",
components: {
'child-component': {
template: "<div>Editing? {{isEditing}}</div>",
props: {
isEditing: {
required: true,
type: Boolean
}
}
}
},
data(){
return {
editMode: true,
schema: {
disabled: false
}
}
},
methods: {
change(){
this.editMode = !this.editMode;
}
},
watch: {
editMode(n){
this.schema.disabled = !n;
}
}
}
</script>
Thanks for your response! I think I havent described my problem enough. I understood that a variable isnt referencable only the objects. I post the answer I got in the Vue forum, in the hope I can help someone else, because this solved my problem. Solution
<template>
<form-generator :schema="schema">
</template>
<script>
Data() {
return {
EditMode: false,
},
methods: {
Edit() {
this.EditMode = true.
}
},
computed: {
schema() {
return [
{
type: "MInput",
disabled: !this.EditMode,
}
]
}
},
</script>

operating on props in vue.js components

Im pretty newm to vue and i'm trying to migrate the frontend of my laravel project to vue but i'm having an issue with it, i'm trying to loop through an array of provided objects called rooms and create divs for each of the in my component as well as setting the default room_id as the first id of the room. The problem is when is access the provided prop array called 'room' in the dom (html) it works flawlessly, but in my vue code for the component file it always seems to be undefined or empty
Here is my components vue code:
export default {
created() {
//this.loadMessages(this.room_id)
console.log(this.first_room) //undefined;
console.log(this.rooms) //empty array;
},
props: ['rooms','first_room'],
computes:{
myrooms: function(){
return this.first_room;
}
},
data()
{
return{
messages: [],
newMessage: '',
room_id: 1 //for test purposes, this works,
}
},
methods: {
loadMessages(id)
{
axios.get('/messages/'+id).then(response => {
this.messages = response.data;
console.log(response.data);
});
}
}
}
the important part of the component html
<div v-for="room in rooms">
<div class="chat-user room">
<div v-for="other in room.others">
<img class="chat-avatar img-circle" :src="other.image" alt="image" >
<div class="chat-user-name">
<a :href="'/user/' + other.id">{{ other.name}}</a>
</div>
</div>
</div>
</div>
//this all works, just for reference
method where i set the values passed to the prop in my main vue instance
EDIT: THE PARENT INSTANCE CODE
Oh and i cant seem too access the rooms array being passed as it is always empty IN code but it loops in the html
window.App.app= new Vue({
el: '#wrapper',
data: {
messages: [],
rooms: [],
test: 'yes',
first: ''
},
created() {
this.fetchRooms();
this.first = 1;
},
methods: {
fetchMessages(id) {
console.log(id);
},
fetchRooms()
{
axios.get('/rooms').then(response => {
this.rooms = response.data;
});
}
}
});
finally where i call my component
<chat-messages :rooms="rooms" :first_room="1"></chat-messages>
//variables referenced from main vue instance
I have literally torn most of my hair on this, please any help is appreciated
In the child component to which the props are passed on.
export default {
created() {
console.log(this.first_room) //undefined;
},
props: ['rooms','first_room'],
computed :{
myrooms: function(){
return this.first_room;
}
},
data () {
return {
messages: [],
newMessage: '',
room_id: 1 //for test purposes, this works,
}
},
watch: {
rooms (n, o) {
console.log(n, o) // n is the new value, o is the old value.
}
},
methods: {
loadMessages (id) {
axios.get('/messages/'+id).then(response => {
this.messages = response.data;
console.log(response.data);
});
}
}
}
You can add a watch on data properties or computed to see the change in their values.
In the question, (as what it appears to be the case), you have consoled the value of the props in the created lifecycle, the props' value gets changed by an API call in the parent component, after the creation of the child component. That explains why your template shows the data but not in the console in the created lifecycle hook.

Merge Vue props with default values

I have an options prop in my Vue component that has a default value.
export default {
props: {
options: {
required: false,
type: Object,
default: () => ({
someOption: false,
someOtherOption: {
a: true,
b: false,
},
}),
},
},
};
If the options object is passed as a prop to the component, the default value is replaced. For example, when passed { someOption: true }, now the options object contains only that value.
How can I pass a partial object and override the default values with the given values instead of replacing the whole object?
I've encountered a similar problem recently and used Object.assign
Here is the docs from mozilla https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
A concrete usage of your case would be something like that:
props: {
options: {
required: false,
type: Object,
default: () => ({}),
},
},
data(){
mergedOptions:{},
defaultOptions:{
someOption: false,
someOtherOption: {
a: true,
b: false,
},
}
},
mounted(){
//you will have the combined options inside mergedOptions
Object.assign(this.mergedOptions,this.defaultOptions,this.options)
}
By doing this, you will override only the properties that passed via props. Don't know if it's the most efficient way but it's very understandable and neat :)
So if you pass in as props :options={someOption:true} the merged options will be equivalent to:
{
someOption: true,
someOtherOption: {
a: true,
b: false,
},
}
EDIT: If you need your data to be reactive, you might want to have a computed.
computed: {
mergedOptions(){
return {
...this.defaultOptions,
...this.options
}
}
}
You will actually never want to modify props within components. If you do, you break one-way-dataflow of parent/child components and your code will be hard to reason about what is affecting what.
Lifted right from the Vue docs, the correct solution is to either (1) use an initial prop or (2) a computed value, so your app can be reactive and respect parent components, and you can rest easy and kick your feet up :)
Both solutions assume your template will use opts for options...
Solution 1: Use an initial prop (defaults and options):
props: ['options', 'defaults'],
data: function () {
var opts = {}
Object.assign(opts, this.defaults, this.options)
return {
opts: opts
}
}
Solution 2: Define a computed property so your component can react to prop changes:
props: ['options', 'defaults'],
computed: {
opts: function () {
let opts = {}
Object.assign(opts, this.defaults, this.options)
return opts
}
}
A quick thought experiement will show, if a parent component changes your input props, your component can properly react.

What's the correct way to pass props as initial data in Vue.js 2?

So I want to pass props to an Vue component, but I expect these props to change in future from inside that component e.g. when I update that Vue component from inside using AJAX. So they are only for initialization of component.
My cars-list Vue component element where I pass props with initial properties to single-car:
// cars-list.vue
<script>
export default {
data: function() {
return {
cars: [
{
color: 'red',
maxSpeed: 200,
},
{
color: 'blue',
maxSpeed: 195,
},
]
}
},
}
</script>
<template>
<div>
<template v-for="car in cars">
<single-car :initial-properties="car"></single-car>
</template>
</div>
</template>
The way I do it right now it that inside my single-car component I'm assigning this.initialProperties to my this.data.properties on created() initialization hook. And it works and is reactive.
// single-car.vue
<script>
export default {
data: function() {
return {
properties: {},
}
},
created: function(){
this.data.properties = this.initialProperties;
},
}
</script>
<template>
<div>Car is in {{properties.color}} and has a max speed of {{properties.maxSpeed}}</div>
</template>
But my problem with that is that I don't know if that's a correct way to do it? Won't it cause me some troubles along the road? Or is there a better way to do it?
Thanks to this https://github.com/vuejs/vuejs.org/pull/567 I know the answer now.
Method 1
Pass initial prop directly to the data. Like the example in updated docs:
props: ['initialCounter'],
data: function () {
return {
counter: this.initialCounter
}
}
But have in mind if the passed prop is an object or array that is used in the parent component state any modification to that prop will result in the change in that parent component state.
Warning: this method is not recommended. It will make your components unpredictable. If you need to set parent data from child components either use state management like Vuex or use "v-model". https://v2.vuejs.org/v2/guide/components.html#Using-v-model-on-Components
Method 2
If your initial prop is an object or array and if you don't want changes in children state propagate to parent state then just use e.g. Vue.util.extend [1] to make a copy of the props instead pointing it directly to children data, like this:
props: ['initialCounter'],
data: function () {
return {
counter: Vue.util.extend({}, this.initialCounter)
}
}
Method 3
You can also use spread operator to clone the props. More details in the Igor answer: https://stackoverflow.com/a/51911118/3143704
But have in mind that spread operators are not supported in older browsers and for better compatibility you'll need to transpile the code e.g. using babel.
Footnotes
[1] Have in mind this is an internal Vue utility and it may change with new versions. You might want to use other methods to copy that prop, see How do I correctly clone a JavaScript object?.
My fiddle where I was testing it:
https://jsfiddle.net/sm4kx7p9/3/
In companion to #dominik-serafin's answer:
In case you are passing an object, you can easily clone it using spread operator(ES6 Syntax):
props: {
record: {
type: Object,
required: true
}
},
data () { // opt. 1
return {
recordLocal: {...this.record}
}
},
computed: { // opt. 2
recordLocal () {
return {...this.record}
}
},
But the most important is to remember to use opt. 2 in case you are passing a computed value, or more than that an asynchronous value. Otherwise the local value will not update.
Demo:
Vue.component('card', {
template: '#app2',
props: {
test1: null,
test2: null
},
data () { // opt. 1
return {
test1AsData: {...this.test1}
}
},
computed: { // opt. 2
test2AsComputed () {
return {...this.test2}
}
}
})
new Vue({
el: "#app1",
data () {
return {
test1: {1: 'will not update'},
test2: {2: 'will update after 1 second'}
}
},
mounted () {
setTimeout(() => {
this.test1 = {1: 'updated!'}
this.test2 = {2: 'updated!'}
}, 1000)
}
})
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.17/dist/vue.js"></script>
<div id="app1">
<card :test1="test1" :test2="test2"></card>
</div>
<template id="app2">
<div>
test1 as data: {{test1AsData}}
<hr />
test2 as computed: {{test2AsComputed}}
</div>
</template>
https://jsfiddle.net/nomikos3/eywraw8t/281070/
I believe you are doing it right because it is what's stated in the docs.
Define a local data property that uses the prop’s initial value as its initial value
https://vuejs.org/guide/components.html#One-Way-Data-Flow
Second or third time I run into that problem coming back to an old vue project.
Not sure why it is so complicated in vue, but it can we done via watch:
export default {
props: ["username"],
data () {
return {
usernameForLabel: "",
}
},
watch: {
username: {
immediate: true,
handler (newVal, oldVal) {
this.usernameForLabel = newVal;
}
},
},
Just as another approach, I did it through watchers in the child component.
This way is useful, specially when you're passing an asynchronous value, and in your child component you want to bind the passed value to v-model.
Also, to make it reactive, I emit the local value to the parent in another watcher.
Example:
data() {
return {
properties: {},
};
},
props: {
initial-properties: {
type: Object,
default: {},
},
},
watch: {
initial-properties: function(newVal) {
this.properties = {...newVal};
},
properties: function(newVal) {
this.$emit('propertiesUpdated', newVal);
},
},
This way I have more control and also less unexpected behaviour. For example, when props that passed by the parent is asynchronous, it may not be available at the time of created or mounted lifecycle. So you can use computed property as #Igor-Parra mentioned, or watch the prop and then emit it.
Following up on Cindy's comment on another answer:
Be carful. The spread operator only shallow clones, so for objects
that contain objects or arrays you will still copy pointers instead of
getting a new copy.
Indeed this is the case. Changes within objects inside arrays will still propagate to your components even when a spread operator is employed.
Here was my solution (using Composition API):
setup() {
properties = ref([])
onMounted(() => {
properties.value = props.initialProperties.map((obj) => ({ ...obj }));
})
}
This worked to set the values and prevent them from getting changed, even if the data was changed in the parent component.

Categories

Resources