Access to props in data - javascript

can you tell me what I do wrong? I need access to props in my data object in component.
I have defined component like this:
export default {
components: {...},
computed: {...},
props: {
userCode: {
type: String,
default: null
}
},
data: () => ({
options: {
callback: function() {
console.log(this.userCode) // prints undefined
return ...;
}
}
}),
methods: {...},
...,
}
prop value I define in router like this:
{
path: '/user/bbb',
name: 'users',
component: userView,
meta: {
requiresLoggedIn: true,
},
props: {userCode: 'XXX'}
}
When I tried in same component render this prop in html like this {{this.userCode}} so it's worked and display my passed code. How to access to prop in options data object? Thanks.

Vue.js best practices aside the reason this.userCode is undefined is because in that case the callback function defines its own this and the global this is not being used. To use the global this either use
callback: () => {}
or
callback: function() {
console.log(this.userCode) // prints undefined
return ...;
}.bind(this)
you can read more about the arrow function here https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions

Related

Vue prop undefined in initial call to computed functions

I have the following Vue component:
Vue.component('result', {
props: ['stuff'],
data: () => ({}),
template: "<img :src='tag' class='result'></img>",
computed: {
tag: function tag() {
return `pages/search/img/${this.stuff.type.toLowerCase()}_tag.png`;
}
}
});
When the component is created, an error is thrown:
TypeError: Cannot read property 'toLowerCase' of undefined
at VueComponent.tag
However, when I remove the call to toLowerCase(), the method executes properly, generating a string with the expected type. I could work around this by changing my filenames to have capital letters, but I would rather understand why Vue is behaving this way. Why would a property be undefined only when methods are called on it?
Update: after some troubleshooting, I found that this.stuff.type is undefined the first time tag() is computed. Calling toLowerCase() just forces an error on an otherwise silent bug. Is there a reason the props aren't defined when computed functions are called for the first time? Should I be writing my component differently?
The stuff prop is undefined when the result component is created.
There are two options to fix this problem:
Either use the v-if directive in the parent component's template to make sure stuff has a value when the result component is created:
<template>
<result v-if="stuff" :stuff="stuff" />
</template>
Or handle the stuff prop being undefined in the result component.
Vue.component('result', {
props: {
// Object with a default value
stuff: {
type: Object,
// Object or array defaults must be returned from
// a factory function
default: () => ({ type: 'someType'})
},
},
data: () => ({}),
template: "<img :src='tag' class='result' >",
computed: {
tag: function tag() {
return `pages/search/img/${this.stuff.type.toLowerCase()}_tag.png`;
}
}
})
Note: The img element is a void element, it does not require an end tag.
Props are by default null but you can give them a default value to overcome this problem.
Example :
Vue.component('result', {
props: {
stuff: {
type: Object,
default: {
type: ''
}
}
},
data: () => ({}),
template: "<img :src='tag' class='result'></img>",
computed: {
tag: function tag() {
return `pages/search/img/${this.stuff.type.toLowerCase()}_tag.png`;
}
}
});

call function inside data() property

I'm trying to fetching some data for my search tree and i'm not able to get the data directly from axios or to call a function because it can't find this.
export default {
name: 'SideNavMenu',
data () {
return {
searchValue: '',
treeData: this.getData(),
treeOptions: {
fetchData(node) {
this.onNodeSelected(node)
}
},
}
},
In the data() I have treeOptions where I want to call a function called onNodeSelected. The error message is:
"TypeError: this.onNodeSelected is not a function"
can anybody help?
When using this, you try to call on a member for the current object.
In JavaScript, using the {} is actually creating a new object of its own and therefore, either the object needs to implement onNodeSelected or you need to call a different function that will allow you to call it on an object that implements the function.
export default {
name: 'SideNavMenu',
data () {
return {
searchValue: '',
treeData: this.getData(), // <--- This
treeOptions: {
fetchData(node) {
this.onNodeSelected(node) // <--- and this
}
},
}
},
//are calling functions in this object :
{
searchValue: '',
treeData: this.getData(),
treeOptions: {
fetchData(node) {
this.onNodeSelected(node)
}
},
//instead of the object you probably are thinking
I would avoid creating object blocks within object blocks like those as the code quickly becomes unreadable and rather create functions within a single object when needed.
I am guessing you would have the same error message if you tried to get a value from treeData as well
You are not calling the function, or returning anything from it. Perhaps you're trying to do this?
export default {
name: 'SideNavMenu',
data () {
return {
searchValue: '',
treeData: this.getData(),
treeOptions: fetchData(node) {
return this.onNodeSelected(node)
},
}
},
Regardless, it is not considered good practice to put functions inside data properties.
Try declaring your variables with empty values first, then setting them when you get the data inside beforeCreate, created, or mounted hooks, like so:
export default {
name: 'SideNavMenu',
data () {
return {
searchValue: '',
treeData: [],
treeOptions: {},
}
},
methods: {
getData(){
// get data here
},
fetchData(node){
this.onNodeSelected(node).then(options => this.treeOptions = options)
}
},
mounted(){
this.getData().then(data => this.treeData = data)
}
},
Or if you're using async await:
export default {
name: 'SideNavMenu',
data () {
return {
searchValue: '',
treeData: [],
treeOptions: {},
}
},
methods: {
getData(){
// get data here
},
async fetchData(node){
this.treeOptions = await this.onNodeSelected(node)
}
},
async mounted(){
this.treeData = await this.getData()
}
},

How can I access this.$route from within vue-apollo?

I'm constructing a GraphQL query using vue-apollo and graphql-tag.
If I hardcode the ID I want, it works, but I'd like to pass the current route ID to Vue Apollo as a variable.
Does work (hardcoded ID):
apollo: {
Property: {
query: PropertyQuery,
loadingKey: 'loading',
variables: {
id: 'my-long-id-example'
}
}
}
However, I'm unable to do this:
Doesn't work (trying to access this.$route for the ID):
apollo: {
Property: {
query: PropertyQuery,
loadingKey: 'loading',
variables: {
id: this.$route.params.id
}
}
}
I get the error:
Uncaught TypeError: Cannot read property 'params' of undefined
Is there any way to do this?
EDIT: Full script block to make it easier to see what's going on:
<script>
import gql from 'graphql-tag'
const PropertyQuery = gql`
query Property($id: ID!) {
Property(id: $id) {
id
slug
title
description
price
area
available
image
createdAt
user {
id
firstName
lastName
}
}
}
`
export default {
name: 'Property',
data () {
return {
title: 'Property',
property: {}
}
},
apollo: {
Property: {
query: PropertyQuery,
loadingKey: 'loading',
variables: {
id: this.$route.params.id // Error here!
}
}
}
}
</script>
You can't have access to "this" object like that:
variables: {
id: this.$route.params.id // Error here!
}
But you can like this:
variables () {
return {
id: this.$route.params.id // Works here!
}
}
Readimg the documentation( see Reactive parameters section) of vue-apollo you can use vue reactive properties by using this.propertyName. So just initialize the route params to a data property as then use it in you apollo object like this
export default {
name: 'Property',
data () {
return {
title: 'Property',
property: {},
routeParam: this.$route.params.id
}
},
apollo: {
Property: {
query: PropertyQuery,
loadingKey: 'loading',
// Reactive parameters
variables() {
return{
id: this.routeParam
}
}
}
}
}
While the accepted answer is correct for the poster's example, it's more complex than necessary if you're using simple queries.
In this case, this is not the component instance, so you can't access this.$route
apollo: {
Property: gql`{object(id: ${this.$route.params.id}){prop1, prop2}}`
}
However, you can simply replace it with a function, and it will work as you might expect.
apollo: {
Property () {
return gql`{object(id: ${this.$route.params.id}){prop1, prop2}}`
}
}
No need for setting extra props.

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