Vue computed visible on DOM render, rather than before - vue.js

If I am manipulating data (shuffling), which is passed in as a prop to a component, within the composition API script setup (which is essentially the old Vue2 created() hook) why do I see the shuffle happen, rather than before the DOM is rendered?
<script setup>
const props = defineProps({
categories: Array,
lexiconTerms: Array
})
const allTerms = computed(() => {
const array = []
if (props.categories) array.push(...props.categories)
if (props.lexiconTerms) {
const lexiconTermsShuffled = props.lexiconTerms.sort(() => Math.random() - 0.5)
array.push(...lexiconTermsShuffled)
}
return array
})
</script>

allTerms is a computed value which gets computed when the component is mounted. If you want to do the computation before mounting the component, duplicate the logic from the computed callback inside a mounted callback.

Related

At what point are props contents available (and are they reactive once they are)?

I pass data into a component via props:
<Comment :comment="currentCase.Comment" #comment="(c) => currentCase.Comment=c"></Comment>
currentCase is updated via a fetch call to an API during the setup of the component (the one that contains the line above)
The TS part of <Comment> is:
<script lang="ts" setup>
import { Comment } from 'components/helpers'
import { ref, watch } from 'vue'
const props = defineProps<{comment: Comment}>()
const emit = defineEmits(['comment'])
console.log(props)
const dateLastUpdated = ref<string>(props.comment?.DateLastUpdated as string)
const content = ref<string>(props.comment?.Content as string)
watch(content, () => emit('comment', {DateLastUpdated: dateLastUpdated, Content: content}))
</script>
... where Comment is defined in 'components/helpers' as
export class Comment {
DateLastUpdated?: string
Content?: string
public constructor(init?: Partial<Case>) {
Object.assign(this, init)
}
}
content is used in the template, but is empty when the component is rendered. I added a console.log() to check whether the props were known - and what is passed is undefined at that point:
▸ Proxy {comment: undefined}
When looking at the value of the props once the application is rendered, their content is correct:
{
"comment": {
"DateLastUpdated": "",
"Content": "comment 2 here"
}
}
My question: why is comment not updated when props are available (and when are their content available?)
I also tried to push the update later in the reactive cycle, but the result is the same:
const dateLastUpdated = ref<string>('')
const content = ref<string>('')
onMounted(() => {
console.log(props)
dateLastUpdated.value = props.comment?.DateLastUpdated as string
content.value = props.comment?.Content as string
watch(content, () => emit('comment', {DateLastUpdated: dateLastUpdated, Content: content}))
})
Vue lifecycle creates component instances from parent to child, then mounts them in the opposite order. Prop value is expected to be available in a child if it's available at this time in a parent. If currentCase is set asynchronously in a parent, the value it's set to isn't available on component creation, it's a mistake to access it early.
This disables the reactivity:
content.value = props.comment?.Content as string
props.comment?.Content === undefined at the time when this code is evaluated, it's the same as writing:
content.value = undefined;
Even if it weren't undefined, content wouldn't react to comment changes any way, unless props.comment is explicitly watched.
If content is supposed to always react to props.comment changes, it should be computed ref instead:
const content = computed(() => props.comment?.Content as string);
Otherwise it should be a ref and a watcher:
const content = ref();
const unwatch = watchEffect(() => {
if (props.comment?.Content) {
content.value = props.comment.Content;
unwatch();
...
}
});

How to access a components $el.innerHTML in Vue 3?

In Vue 2 it was possible to access the innerHTML of a Vue component instance via someInstance.$el.innerHTML. How can the same be achieved in Vue 3?
Let's say you want to to create a Vue component and access its innerHTML. In Vue 2, this could be done like so:
const wrapper = document.createElement('div');
const someComponentInstance = new Vue({
render: h => h(SomeComponent, {
props: {
someProp: 'prop-value-123'
}
})
});
someComponentInstance.$mount(wrapper);
console.log(someComponentInstance.$el.innerHTML);
To achieve the same thing in Vue 3, we have to leverage the createApp() and mount() functions like so:
const wrapper = document.createElement('div');
const someComponentInstance = createApp(SomeComponent, {
someProp: 'prop-value-123'
});
const mounted = someComponentInstance.mount(wrapper); // returns an instance containing `$el.innerHTML`
console.log(mounted.$el.innerHTML);
A word of warning: Make sure your innerHTML is sanitized if it is user generated and you want to reuse it somewhere in your app.

In Vue, what if I need to use state from getters while mounted life cycle hook?

I try to use data from vuex during mounted lifecycle hook.
However, it seems like mounted life cycle hook is excuted before I get the data from vuex.
How do I access the data from vuex and use it during mounted life cycle hook?
The code is as below.
I bring data by getters like this.
computed:{
targetCounty(){
return this.$store.getters['parkingModule/byCounty'][this.countyname]
}
Then I need to feed this data to my class constructur by init() method
init(){
scene =new THREE.Scene();
const canvas=document.querySelector('#myCanvas');
canvas.width=innerWidth;
canvas.height=innerHeight;
camera = new THREE.PerspectiveCamera( 75, window.innerWidth/window.innerHeight, 0.1,
1000 );
renderer=new THREE.WebGLRenderer({canvas:canvas})
renderer.setSize( window.innerWidth, window.innerHeight );
let texture = new THREE.TextureLoader().load('disc.png');
let rawRad = this.rawRadius
console.log(this.targetCounty)
const meshobject =new
ParkingSpot(rawRad,this.targetCounty.length,100,texture,this.targetCounty)
sphereMesh= meshobject.createMesh();
camera.position.z = 5
scene.add(sphereMesh);
console.log(sphereMesh.material.size)
},
this init() method is invoked during mounted life cycle hook like this.
mounted(){
this.init()
this.animate();
// window.addEventListener()
},
created(){
console.log(this.targetCounty)
// setTimeout(()=>{console.log(this.targetCounty[0])},3000)
},
However, when I log this.targetCounty, it returns empty array. So I got around it
by rendering computed property in DOM cause computed property runs only the element is rendered.
<template>
<div>
<canvas id="myCanvas"></canvas>
</div>
<p v-show='false'>{{targetCounty}}</p>
</template>
I created dummy DOM element only to get the computed property for my mounted life cycle(I think it's very bad approach)
What would be the solution for solving this problem?
You could use vm.$watch() in the mounted() hook to observe the store's getter for the initial value:
export default {
mounted() {
const unwatch = this.$watch(
() => this.$store.getters['parkingModule/byCounty'][this.countyname],
targetCounty => {
if (targetCounty) {
// handle initial value here...
this.targetCounty = targetCounty
this.init()
this.animate()
unwatch()
}
}
)
}
}
demo
Why don't you try making a function that explicitly returns the value and then invoke it in the mounted() lifecycle hook, saving it into a constant. Then pass that constant into your init function.
const targetCountry = this.$store.getters['parkingModule/byCounty'[this.countyname]
this.init(targetCountry)

Vue composition api is not updating bound value to text field

I am updating an attribute of an object after initialization.
My dumbed-down component looks like this:
<template lang="pug">
div
v-text-field(v-model="object.name")
v-text-field(v-model="object.vpnPort")
</template>
<script>
import { ref } from '#vue/composition-api'
export default {
setup(props, { root }) {
const object = ref({})
getNextPort().then(response => (object.value.vpnPort = response.data))
return { object }
}
}
</script>
In this example, getNextPort is an API call that returns a number. For some reason, the v-text-field is not updated. I do not see the value in the input field. When I console.log the object after the getNextPort call it shows:
{"vpnPort":10001}
Which is the expected result. I also tried:
$nextTick
onMounted
$forceUpdate
But when I start typing in the name field the vpnPort doest get updated!
Does anybody know why the result is not shown in the v-text-field?
You should initialize your object data with empty fields like :
const object = ref({
name:'',
vpnPort:null
})
for reactive objects, you should use reactive
const object = reactive({
name: '',
vpnPort: null
})
change
object.value.vpnPort = response.data
to
object.vpnPort = response.data
check out https://composition-api.vuejs.org/#ref-vs-reactive for more info

Is there a way to access Vue $scopedSlot's mounted DOM element?

I have a scoped slot named myElement, I can access it via this.$scopedSlots.myElement()[0].
Usually when a vnode is mounted there is a $el containing the DOM element, but not in scoped slots apparently, there is only an undefined elm.
I also tried to put a ref on the slot in the child template and access it through .context.$refs but it lists the refs on the parent template only.
Is there any way I can access that mounted DOM element, if its id or class is unknown from the child component?
Thanks
More Details:
Parent template (Pug)
child-component
template(v-slot:myelement="{ on }")
span My element content
Child component
mounted () {
console.log(this.$scopedSlots.myelement()[0])
// From here I want to get the position of the span with
// span.getBoundingClientRect()
}
Reproduction link:
https://codepen.io/antoniandre/pen/BaogjrM?editable=true&editors=101
So something like this can be used to workaround:
mounted () {
const myEl = this.$scopedSlots.myelement()[0];
if(myEl.data && myEl.data.attrs && myEl.data.attrs.id){
const id = myEl.data.attrs.id
console.log([this.$el.querySelector('#id')])
} else {
throw new Error('slot myelement needs an id');
}
}
or forcefully apply an id:
const Child = {
render: function(h){
const myEl = this.$scopedSlots.myelement()[0]
myEl.data = {attrs: {id: 'id'}};
console.log(myEl)
return h('div', {}, [myEl])
},
mounted () {
console.log([this.$el.querySelector('#id')])
}
}
The only workaround that I've found so far is this:
First get the vnode of the scopedSlot, get its given context, crawl each vnode children of the context to find itself by the _uid key, which will contain the $el mounted element.
This is not very straightforward but it does the trick.
mounted () {
const { context } = this.$scopedSlots.activator()[0]
console.log(context.$children.find(vnode => vnode._uid === this._uid).$el)
}
I asked in Vue forum and Vue chat but I am still looking for a better way, if there is let me know.