Vue3 props access inside setup() returns nothing - vue.js

I have a weird situation with my Vue3 component. I am trying to get a value from the props inside the setup() function. But this returns nothing.
<template>
<div class="border p-2 space-y-2">
<h2 class="text-center">Makers</h2>
{{ product.makers }}
</div>
</template>
<script>
import { ref, onMounted, toRef, toRefs } from 'vue'
import ProductStore from '../store/ProductStore'
export default {
props: {
product: Object
},
setup(props) {
console.log(props.product.makers)
},
}
</script>
The value is sent from another component like the one below
<ProductMakers :product="product"/>
But I always get undefined as my response. Any clue to resolve this problem? Am I missing something?
To my surprise, the template is always showing the value correctly. The problem is only inside the setup(). Any clue?

This means that product is reactively updated after the creation of component instance.
Updated value is supposed to be accessed in a watcher or a computed. In case a side effect like logging is needed, this is the case for a watcher:
setup(props) {
watchEffect(() => {
if (props.product.makers)
console.log(props.product.makers)
});
},

Related

vue3: extends missing parent's setup

parent.vue
<template>
<input :type="computedType"/>
</template>
<script setup>
import { ref, computed } from 'vue';
const props = defineProps({
type: {
required: false,
type: String,
default: 'text',
},
});
const showPassword = ref(false);
const computedType = computed(() => {
if (props.type === 'password') {
return showPassword.value ? 'text' : 'password';
}
return props.type;
});
</script>
<script>
export default {
data() {
return {
uuid: getRandomUuid(),
}
}
}
</script>
child.vue
<template>
<input :type="computedType"/>
</template>
<script>
import Parent from '~/components/parent.vue'
export default {
extends: Parent
}
</script>
In Vue3, I have a child.vue component which is inherited from parent.vue, I defined a computedType computed attribute in parent.vue, but it's missing in child.vue, however, uuid which is defined in parent.vue is accessible in child.vue.
[Vue warn]: Property "computedType" was accessed during render but is not defined on instance.
How to get computedType and any other attributes defined in <script setup> of parent.vue in child.vue?
Really appreciate any help provided!
update:
Only if I define all the attributes in <script>(but not in setup()) instead of <script setup>, they could be accessible
There are a few specific instances where you can have more than one script tag, and they are all outlined here in the documentation. Still, besides those 3 specific use cases, you shouldn't use more than one script tag. Your case of a separate script tag for one data variable is not a valid use case.
I recommend writing the component entirely with the options API (the only API that supports the "extends" option), or writing entirely with the composition API where you would use composable functions to effectively extend your component
defineExpose({computedType});
try this

Why does a prop change not change data?

I'm trying to understand why changing a prop, used to provide an initial value, doesn't cause the data variable to change.
In the below example, changing the initialName value passed in the parent component, also causes the initialName to change in the child component. However, name keeps the value it was originally initialized as. I believed, seemingly incorrectly, that changing a prop would re-render the component.
ChildComponent.vue
<template>
<div>
{{initialName}}
{{name}}
</div>
</template>
<script>
export default {
props: {
initialName: {
type: String,
default: '',
}
},
data() {
return {
name: this.initialName,
};
},
</script>
ParentComponent.vue
<template>
<ChildComponent :initialName="AnExampleName"/>
</template>
<script>
import ChildComponent from ChildComponent.vue
export default {
components: {
ChildComponent
}
</script>
I've been able to work around this by watching the prop and updating name, however this doesn't feel like the best approach.
How come changing the prop doesn't change the data? Is there a better way to pass an initial value to a child component?
data is meant to be static, so if you set it once, it will not be reactive afterwards.
It will change if you mutate it directly of course, like this this.name = 'updated value hi hi'.
But it will not mutate if you update another prop/state elsewhere (like initialName in your example).
A simple approach for this would be to use a computed like this
<script>
export default {
props: {
initialName: {
type: String,
default: "",
},
},
computed: {
name() {
return this.initialName
}
},
};
</script>
This example proves that the usage of data is NOT updating the name value (initialName is totally reactive) if you do use it like the OP did. The usage of computed solves this issue tho.

How to get this.$el of a component correctly, in Vue3?

I have a component, its template looks like this:
<template>
<slot />
</template>
The function I'm trying to make, is about resize-observer-polyfill.
I'm trying to get this.$el in mounted(), then create an instance of ResizeObserver.
But the problem is, if the content of slot is an async-component, then I cannot get this.$el correctly.
How can I fix this?
In Vue 3, you can use a template ref:
<template>
<div ref="el"></div>
</template>
import { ref, onMounted } from 'vue'
export default {
setup() {
const el = ref(null);
// Not available until the component mounts:
onMounted(() => {
console.log(el.value)
})
return {
el
}
}
}

Passing props to Vue root instance via attributes on element the app is mounted on

I am terribly new to Vue, so forgive me if my terminology is off. I have a .NET Core MVC project with small, separate vue pages. On my current page, I return a view from the controller that just has:
#model long;
<div id="faq-category" v-bind:faqCategoryId="#Model"></div>
#section Scripts {
<script src="~/scripts/js/faqCategory.js"></script>
}
Where I send in the id of the item this page will go grab and create the edit form for. faqCategory.js is the compiled vue app. I need to pass in the long parameter to the vue app on initialization, so it can go fetch the full object. I mount it with a main.ts like:
import { createApp } from 'vue'
import FaqCategoryPage from './FaqCategoryPage.vue'
createApp(FaqCategoryPage)
.mount('#faq-category');
How can I get my faqCategoryId into my vue app to kick off the initialization and load the object? My v-bind attempt seems to not work - I have a #Prop(Number) readonly faqCategoryId: number = 0; on the vue component, but it is always 0.
My FaqCategoryPAge.vue script is simply:
<script lang="ts">
import { Options, Vue } from "vue-class-component";
import { Prop } from 'vue-property-decorator'
import Card from "#/Card.vue";
import axios from "axios";
import FaqCategory from "../shared/FaqCategory";
#Options({
components: {
Card,
},
})
export default class FaqCategoryPage extends Vue {
#Prop(Number) readonly faqCategoryId: number = 0;
mounted() {
console.log(this.faqCategoryId);
}
}
</script>
It seems passing props to root instance vie attributes placed on element the app is mounting on is not supported
You can solve it using data- attributes easily
Vue 2
const mountEl = document.querySelector("#app");
new Vue({
propsData: { ...mountEl.dataset },
props: ["message"]
}).$mount("#app");
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app" data-message="Hello from HTML">
{{ message }}
</div>
Vue 3
const mountEl = document.querySelector("#app");
Vue.createApp({
props: ["message"]
}, { ...mountEl.dataset }).mount("#app");
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.0.0/vue.global.js"></script>
<div id="app" data-message="Hello from HTML">
{{ message }}
</div>
Biggest disadvantage of this is that everything taken from data- attributes is a string so if your component expects something else (Number, Boolean etc) you need to make conversion yourself.
One more option of course is pushing your component one level down. As long as you use v-bind (:counter), proper JS type is passed into the component:
Vue.createApp({
components: {
MyComponent: {
props: {
message: String,
counter: Number
},
template: '<div> {{ message }} (counter: {{ counter }}) </div>'
}
},
}).mount("#app");
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.0.0/vue.global.js"></script>
<div id="app">
<my-component :message="'Hello from HTML'" :counter="10" />
</div>
Just an idea (not a real problem)
Not really sure but it can be a problem with Props casing
HTML attribute names are case-insensitive, so browsers will interpret any uppercase characters as lowercase. That means when you're using in-DOM templates, camelCased prop names need to use their kebab-cased (hyphen-delimited) equivalents
Try to change your MVC view into this:
<div id="faq-category" v-bind:faq-category-id="#Model"></div>
Further to Michal LevĂ˝'s answer regarding Vue 3, you can also implement that pattern with a Single File Component:
app.html
<div id="app" data-message="My Message"/>
app.js
import { createApp } from 'vue';
import MyComponent from './my-component.vue';
const mountEl = document.querySelector("#app");
Vue.createApp(MyComponent, { ...mountEl.dataset }).mount("#app");
my-component.vue
<template>
{{ message }}
</template>
<script>
export default {
props: {
message: String
}
};
</script>
Or you could even grab data from anywhere on the parent HTML page, eg:
app.html
<h1>My Message</h1>
<div id="app"/>
app.js
import { createApp } from 'vue';
import MyComponent from './my-component.vue';
const message = document.querySelector('h1').innerText;
Vue.createApp(MyComponent, { message }).mount("#app");
my-component.vue
<template>
{{ message }}
</template>
<script>
export default {
props: {
message: String
}
};
</script>
To answer TheStoryCoder's question: you would need to use a data prop. My answers above demonstrate how to pass a value from the parent DOM to the Vue app when it is mounted. If you wanted to then change the value of message after it was mounted, you would need to do something like this (I've called the data prop myMessage for clarity, but you could also just use the same prop name message):
<template>
{{ myMessage }}
<button #click="myMessage = 'foo'">Foo me</button>
</template>
<script>
export default {
props: {
message: String
},
data() {
return {
myMessage: this.message
}
}
};
</script>
So I'm not at all familiar with .NET and what model does, but Vue will treat the DOM element as a placeholder only and it does not extend to it the same functionality as the components within the app have.
so v-bind is not going to work, even without the value being reactive, the option is not there to do it.
you could try a hack to access the value and assign to a data such as...
const app = Vue.createApp({
data(){
return {
faqCategoryId: null
}
},
mounted() {
const props = ["faqCategoryId"]
const el = this.$el.parentElement;
props.forEach((key) => {
const val = el.getAttribute(key);
if(val !== null) this[key] = (val);
})
}
})
app.mount('#app')
<script src="https://unpkg.com/vue#3.0.0-rc.11/dist/vue.global.prod.js"></script>
<div id="app" faqCategoryId="12">
<h1>Faq Category Id: {{faqCategoryId}}</h1>
</div>
where you get the value from the html dom element, and assign to a data. The reason I'm suggesting data instead of props is that props are setup to be write only, so you wouldn't be able to override them, so instead I've used a variable props to define the props to look for in the dom element.
Another option
is to use inject/provide
it's easier to just use js to provide the variable, but assuming you want to use this in an mvc framework, so that it is managed through the view only. In addition, you can make it simpler by picking the exact attributes you want to pass to the application, but this provides a better "framework" for reuse.
const mount = ($el) => {
const app = Vue.createApp({
inject: {
faqCategoryId: {
default: 'optional'
},
},
})
const el = document.querySelector($el)
Object.keys(app._component.inject).forEach(key => {
if (el.getAttribute(key) !== null) {
app.provide(key, el.getAttribute(key))
}
})
app.mount('#app')
}
mount('#app')
<script src="https://unpkg.com/vue#3.0.0-rc.11/dist/vue.global.prod.js"></script>
<div id="app" faqCategoryId="66">
<h1>Faq Category Id: {{faqCategoryId}}</h1>
</div>
As i tried in the following example
https://codepen.io/boussadjra/pen/vYGvXvq
you could do :
mounted() {
console.log(this.$el.parentElement.getAttribute("faqCategoryId"));
}
All other answers might be valid, but for Vue 3 the simple way is here:
import {createApp} from 'vue'
import rootComponent from './app.vue'
let rootProps = {};
createApp(rootComponent, rootProps)
.mount('#somewhere')

Determining if slot content is null or empty

I have a little Loading component, whose default text I want to be 'Loading...'. Good candidate for slots, so I have something like this as my template:
<p class="loading"><i class="fa fa-spinner fa-spin"></i><slot>Loading...</slot></p>
That allows me to change the loading message with e.g. <loading>Searching...</loading>. The behaviour I would like, though, is not just to display the default message if no slot content is supplied, but also if the slot content is null or blank. At the moment if I do e.g.<loading>{{loadingMessage}}</loading> and loadingMessage is null, no text is displayed (where I want the default text to be displayed). So ideally I need to test this.$slots.default. This tells me whether content was passed in, but how do I find whether or not it was empty? this.$slots.default.text returns undefined.
You'd need a computed property which checks for this.$slots. With a default slot you'd check this.$slots.default, and with a named slot just replace default with the slot name.
computed: {
slotPassed() {
return !!this.$slots.default[0].text.length
}
}
And then use it in your template:
<template>
<div>
<slot v-if="slotPassed">Loading...</slot>
<p v-else>Searching...</p>
</div>
</template>
You can see a small example here. Notice how fallback content is displayed and not "default content", which is inside the slot.
Edit:
My wording could've been better. What you need to do is check for $slots.X value, but computed property is a way to check that. You could also just write the slot check in your template:
<template>
<div>
<slot v-if="!!$slots.default[0].text">Loading...</slot>
<p v-else>Searching...</p>
</div>
</template>
Edit 2: As pointed out by #GoogleMac in the comments, checking for a slot's text property fails for renderless components (e.g. <transition>, <keep-alive>, ...), so the check they suggested is:
!!this.$slots.default && !!this.$slots.default[0]
// or..
!!(this.$slots.default || [])[0]
#kano's answer works well, but there's a gotcha: this.$slots isn't reactive, so if it starts out being false, and then becomes true, any computed property won't update.
The solution is to not rely on a computed value but instead on created and beforeUpdated (as #MathewSonke points out):
export default {
name: "YourComponentWithDynamicSlot",
data() {
return {
showFooter: false,
showHeader: false,
};
},
created() {
this.setShowSlots();
},
beforeUpdate() {
this.setShowSlots();
},
methods: {
setShowSlots() {
this.showFooter = this.$slots.footer?.[0];
this.showHeader = this.$slots.header?.[0];
},
},
};
UPDATE: Vue 3 (Composition API)
For Vue 3, it seems that the way to check whether a slot has content has changed (using the new composition API):
import { computed, defineComponent } from "vue";
export default defineComponent({
setup(_, { slots }) {
const showHeader = computed(() => !!slots.header);
return {
showHeader,
};
},
});
note: I can't find any documentation on this, so take it with a pinch of salt, but seems to work in my very limited testing.
this.$slots can be checked to see if a slot has been used.
It is important to note that this.$slots is not reactive. This could cause problems when using this.$slots in a computed value.
https://v2.vuejs.org/v2/api/?redirect=true#:~:text=Please%20note%20that%20slots%20are%20not%20reactive.
This means we need to ensure that this.slots is checked whenever the component re-renders. We can do this simply by using a method instead of a computed property.
https://v2.vuejs.org/v2/guide/computed.html?redirect=true#:~:text=In%20comparison%2C%20a%20method%20invocation%20will%20always%20run%20the%20function%20whenever%20a%20re%2Drender%20happens
<template>
<div>
<slot v-if="hasHeading" name="heading"/>
</div>
</template>
<script>
export default{
name: "some component",
methods: {
hasHeading(){ return !!this.slots.heading}
}
}
</script>