The lifecycle of functions in Vue3 templates - vue.js

This may be the minimal reproduction of my problem
<template>
<span class='current-page'>
{{ get_page_param('current') }}
</span>
</template>
const get_page_param = function(direction) {
// TODO: need delete the comment
console.log(`get_page_param(${direction}) is calling`);
try {
let url_string = null;
switch (direction) {
case 'next':
url_string = info.value.next;
break;
case 'previous':
url_string = info.value.previous;
break;
default:
return route.query.page;
}
const url = new URL(url_string);
return url.searchParams.get('page');
} catch (err) {
console.log(err);
}
};
onBeforeMount(()=>{
console.log('before mounting');
axios
.get('/api/article')
.then(response => {
info.value = response.data;
});
})
onMounted(() => {
console.log('mounted');
});
When I run this code, I see get_page_param(current) is calling is printed twice in the browser's console, and both of them happen before mounting, and the second is printed after mounted
my question is
Why this function called twice
If the first call is for rendering templates, what is the reason of the second
I checked Vue's official documentation about lifecycle hooks https://cn.vuejs.org/api/composition-api-lifecycle.html#onupdated, but I still don't understand why the page will be execute the function twice during the rendering process. I think it may be front-end related knowledge, but I haven't understood it

The reason why the rendering is called twice is related to the lifecycle of vue components. Vue will always render the component, when a reactive value changes. As you use a function inside a template, this will be called on each render cycle of the component.
In your example the info.value update will trigger a rerender. Also if using a function, vue needs to allways trigger a rerender if you change any reactive value.
That is also why you should not use functions inside the template. Whenever possible you should use computed properties and apply it inside the template. The advantage is, that vue will internally cache the value of the computed property and only update the component, if some of the values you use inside is updated.
You should make sure, you understand how the vue lifecycle works.
A better approach would be to use a computed property that is rendered inside your template. The following shows an example that might work for you.
const direction = ref('');
const pageParam = computed(() => {
try {
let url_string = null;
switch (direction.value) {
case 'next':
url_string = info.value.next;
break;
case 'previous':
url_string = info.value.previous;
break;
default:
return route.query.page;
}
const url = new URL(url_string);
return url.searchParams.get('page');
} catch (err) {
console.log(err);
return '1'; // you need to return the fallback page
}
});
Inside the template you use
<template>
<span class='current-page'>
{{ pageParam }}
</span>
</template>

Related

Vue3: Why when I scroll to element's scrollHeight, the last element is not visible?

I'm trying to make a chat window, where when I send/get a message, the window scrolls to the very bottom. That's how I make it:
template:
<ul class="chat-window">
<li v-for="message in messages">
<span>{{ message.from }}</span>{{ message.message }}
</li>
</ul>
script:
const messages = ref()
socket.on('chat-message', (data) => {
messages.value.push(data)
const chatWindow = document.querySelector('.chat-window')
chatWindow.scrollTop = chatWindow.scrollHeight
})
But when coded this way, the last message is never seen (you need to scroll to it).
I found out that when I use setTimeout, like this:
setTimeout(() => {
const chatWindow = document.querySelector('.chat-window')
chatWindow.scrollTop = chatWindow.offsetHeight
}, 10)
then it works fine. So I know how to make it work, but I don't know why I need to use setTimeout. Can anybody please explain? Is there a better way to do it?
The reason why it works with setTimeout is the lifecycle of a vue component. You should read the documentation page to understand the how the lifecycle works.
So if you set a variable like you do
messages.value.push(data)
Vue needs to run a new lifecycle to update the component. If you access the DOM directly after the change of the value, Vue might not have been updated the component yet.
But, you can actively say, you want to wait for the update to be done with the nextTick function: https://vuejs.org/api/general.html#nexttick
// import nextTick from vue
import { nextTick } from 'vue';
// callback needs to be async
socket.on('chat-message', async (data) => {
messages.value.push(data)
await nextTick();
const chatWindow = document.querySelector('.chat-window')
chatWindow.scrollTop = chatWindow.scrollHeight
})
// depending on the weight of your overall page,
// sometimes you also need to request the animation frame first
// this is better then using setTimeout. But you should try without first.
socket.on('chat-message', async (data) => {
messages.value.push(data)
await nextTick();
requestAnimationFrame(() => {
const chatWindow = document.querySelector('.chat-window')
chatWindow.scrollTop = chatWindow.scrollHeight
});
})
Instead of using querySelector you should use a template ref for the html element as described here: https://vuejs.org/guide/essentials/template-refs.html
<ul ref="chatWindow" class="chat-window">
<li v-for="message in messages">
<span>{{ message.from }}</span>{{ message.message }}
</li>
</ul>
// setup script
const messages = ref([]) // don’t forget the initial empty array here
const chatWindow = ref(null) // template refs are always initialized with null
socket.on('chat-message', async (data) => {
messages.value.push(data)
await nextTick();
// you need to make sure, the chatWindow is not null
if (!chatWindow.value) return;
chatWindow.value.scrollTop = chatWindow.value.scrollHeight
})

How to fire an $emit event from Vue Composable

I have a vue composable that needs to fire an event. I naively set it up as follows:
*// composable.js*
import { defineEmits } from "vue";
export default function useComposable() {
// Vars
let buffer = [];
let lastKeyTime = Date.now();
const emit = defineEmits(["updateState"]);
document.addEventListener("keydown", (e) => {
// code
emit("updateState", data);
}
// *App.vue*
<template>
<uses-composables
v-show="wirtleState.newGame"
#updateState="initVars"
></uses-composables>
</template>
<script setup>
const initVars = (data) => {
//code here
}
// usesComposable.vue
<template>
<button #click="resetBoard" class="reset-button">Play Again</button>
</template>
<script setup>
import { defineEmits } from "vue";
import useEasterEgg from "#/components/modules/wirdle_helpers/useEasterEgg.js";
useEasterEgg();
</script>
The error I get is "Uncaught TypeError: emit is not a function useEasterEgg.js:30:11
So obviously you can not use defineEmits in a .js file. I dont see anywhere in Vue docs where they specifically use this scenario. I dont see any other way to do this but using $emits but that is invoked in a template which my composable does not have. Any enlightenment much appreciated.
You can emit events from a composable, but it will need to know where the events should be fired from using context which can be accessed from the second prop passed to the setup function: https://vuejs.org/api/composition-api-setup.html#setup-context
Composable:
export default function useComposable(context) {
context.emit("some-event")
}
Component script:
<script>
import useComposable from "./useComposable"
export default {
emits: ["some-event"],
setup(props, context) {
useComposable(context)
}
}
</script>
To use it in a script setup, the best way I found was to declare the defineEmit first, and assigning it to a const, and pass it as a param to your composable :
const emit = defineEmit(['example']
useMyComposable(emit);
function useMyComposable(emit){
emit('example')
}
You can't access emit this way, as the doc says : defineProps and defineEmits are compiler macros only usable inside script setup. https://vuejs.org/api/sfc-script-setup.html
I'm not entirely sure of what you are trying to achieve but you can use vue-use composable library to listen to key strokes https://vueuse.org/core/onkeystroke/
Lx4
This question is a bit confusing. What is <uses-composable>? A composable generally is plan js/ts, with no template. If it had a template, it would just be a component. Even if it was a component, which I mean you could turn it into if thats what you wanted, I don't see anything there that suggests that would be a good idea.
So I guess the question is, why would you want to emit an event? If I'm following what you want, you can just do:
// inside useEasterEgg.js
document.addEventListener('keydown', () => {
// other code
const data = 'test';
updateStateCallback(data);
});
function useEasterEgg() {
function onUpdateState(callback) {
updateStateCallback = callback;
}
return {
onUpdateState,
}
}
// Put this wherever you want to listen to the event
const { onUpdateState } = useEasterEgg();
onUpdateState(data => console.log(data));
https://jsfiddle.net/tdfu3em1/3/
Alternatively, if you just want access to data, why not make it a ref and just use it where you want:
const data = ref();
document.addEventListener('keydown', () => {
// other code
data.value = resultOfOtherCode;
});
function useEasterEgg() {
return {
data,
}
}
// Anywhere you want to use it
const { data } = useEasterEgg();

How to generate computed props on the fly while accessing the Vue instance?

I was wondering if there is a way of creating computed props programatically, while still accessing the instance to achieve dynamic values
Something like that (this being undefined below)
<script>
export default {
computed: {
...createDynamicPropsWithTheContext(this), // helper function that returns an object
}
}
</script>
On this question, there is a solution given by Linus: https://forum.vuejs.org/t/generating-computed-properties-on-the-fly/14833/4 looking like
computed: {
...mapPropsModels(['cool', 'but', 'static'])
}
This works fine but the main issue is that it's fully static. Is there a way to access the Vue instance to reach upon props for example?
More context
For testing purposes, my helper function is as simple as
export const createDynamicPropsWithTheContext = (listToConvert) => {
return listToConvert?.reduce((acc, curr) => {
acc[curr] = curr
return acc
}, {})
}
What I actually wish to pass down to this helper function (via this) are props that are matching a specific prefix aka starting with any of those is|can|has|show (I'm using a regex), that I do have access via this.$options.props in a classic parent/child state transfer.
The final idea of my question is mainly to avoid manually writing all the props manually like ...createDynamicPropsWithTheContext(['canSubmit', 'showModal', 'isClosed']) but have them populated programatically (this pattern will be required in a lot of components).
The props are passed like this
<my-component can-submit="false" show-modal="true" />
PS: it's can-submit and not :can-submit on purpose (while still being hacked into a falsy result right now!).
It's for the ease of use for the end user that will not need to remember to prefix with :, yeah I know...a lot of difficulty just for a semi-colon that could follow Vue's conventions.
You could use the setup() hook, which receives props as its first argument. Pass the props argument to createDynamicPropsWithTheContext, and spread the result in setup()'s return (like you had done previously in the computed option):
import { createDynamicPropsWithTheContext } from './props-utils'
export default {
⋮
setup(props) {
return {
...createDynamicPropsWithTheContext(props),
}
}
}
demo
If the whole thing is for avoiding using a :, then you might want to consider using a simple object (or array of objects) as data source. You could just iterate over a list and bind the data to the components generated. In this scenario the only : used are in the objects
const comps = [{
"can-submit": false,
"show-modal": true,
"something-else": false,
},
{
"can-submit": true,
"show-modal": true,
"something-else": false,
},
{
"can-submit": false,
"show-modal": true,
"something-else": true,
},
]
const CustomComponent = {
setup(props, { attrs }) {
return {
attrs
}
},
template: `
<div
v-bind="attrs"
>{{ attrs }}</div>
`
}
const vm = Vue.createApp({
setup() {
return {
comps
}
},
template: `
<custom-component
v-for="(item, i) in comps"
v-bind="item"
></custom-component>
`
})
vm.component('CustomComponent', CustomComponent)
vm.mount('#app')
<script src="https://unpkg.com/vue#3"></script>
<div id="app">{{ message }}</div>
Thanks to Vue's Discord Cathrine and skirtle folks, I achieved to get it working!
Here is the thread and here is the SFC example that helped me, especially this code
created () {
const magicIsShown = computed(() => this.isShown === true || this.isShown === 'true')
Object.defineProperty(this, 'magicIsShown', {
get () {
return magicIsShown.value
}
})
}
Using Object.defineProperty(this... is helping keeping the whole state reactive and the computed(() => can reference some other prop (which I am looking at in my case).
Using a JS object could be doable but I have to have it done from the template (it's a lower barrier to entry).
Still, here is the solution I came up with as a global mixin imported in every component.
// helper functions
const proceedIfStringlean = (propName) => /^(is|can|has|show)+.*/.test(propName)
const stringleanCase = (string) => 'stringlean' + string[0].toUpperCase() + string.slice(1)
const computeStringlean = (value) => {
if (typeof value == 'string') {
return value == 'true'
}
return value
}
// the actual mixin
const generateStringleans = {
created() {
for (const [key, _value] of Object.entries(this.$props)) {
if (proceedIfStringlean(key)) {
const stringleanComputed = computed(() => this[key])
Object.defineProperty(this, stringleanCase(key), {
get() {
return computeStringlean(stringleanComputed.value)
},
// do not write any `set()` here because this is just an overlay
})
}
}
},
}
This will scan every .vue component, get the passed props and if those are prefixed with either is|can|has|show, will create a duplicated counter-part with a prefix of stringlean + pass the initial prop into a method (computeStringlean in my case).
Works great, there is no devtools support as expected since we're wiring it directly in vanilla JS.

vue3 multi language site, possible side effects with computed properties, dynamic components,

I display the header and footer of a website with the help of vuex + vue-router + dynamic components.
I fetch the current routes from vue-router via vuex and "inject" the data into App.vue, where I have my header and footer. I can console.log the correct routes of every page.
The app.vue markup looks like that:
<component :is="selectedHeader"></component>
<router-view></router-view>
<component :is="selectedFooter"></component>
Now I use the computed properties selectedFooter and selectedHeader in order to display the correct components:
computed: {
// eslint-disable-next-line vue/return-in-computed-property
selectedHeader() {
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
this.currentRoute = this.$store.getters.getRoute
if(this.currentRoute) {
if(this.currentRoute.includes("de")) {
return "de-header";
}
if(this.currentRoute.includes("en")) {
return "en-header";
}
if(this.currentRoute.includes("es")) {
return "es-header";
}
}},
// eslint-disable-next-line vue/return-in-computed-property
selectedFooter() {
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
this.currentRoute = this.$store.getters.getRoute
if(this.currentRoute) {
if(this.currentRoute.includes("de")) {
return "de-footer";
}
if(this.currentRoute.includes("en")) {
return "en-footer";
}
else if(this.currentRoute.includes("es")) {
return "es-footer";
}
}},
It pricipally works, but I think that I'm already experiencing side effects because I don't use the computed properties correctly, e.g. one route displays to the wrong language header, even though the route data from the store is definitely correct?
I guess that it would be better to use watchers, but how?

vue3: control property with a timed function

First of all, I am a new vuejs developer and my purpose is to get acquainted with Vue, so, not going to use any external plugins or components.
I am writing a simple alert component, which looks like this:
<Alert :show="showAlert" />
I want the show property to return back to false after 2 seconds. How can I do this from inside the component (i.e., not in the page where this component is used). I tried this:
import { computed } from 'vue';
export default {
props: ['show'],
setup(props) {
const shown = computed(() => {
if (props.show) {
setTimeout(() => {
console.log("hiding the alert...")
props.show = false
}, 2000);
}
return props.show.value
})
return { shown }
}
};
the compiler said:
14:15 error Unexpected timed function in computed function vue/no-async-in-computed-properties
16:19 error Unexpected mutation of "show" prop vue/no-mutating-props
My rational is that the delay of alert should be controlled by the alert component (which could be changed by a prop), but not forcing the caller to write some thing like:
function Alert(delay) {
showAlert = true
setTimeout(() => showAlert = false, delay)
}
There are 2 errors.
First vue/no-mutating-props, props are read only so you are not supposed to change it from within the component. It is still possible to change props from outside the component and pass down to it.
For this you should copy the value of props to your data()
data() {
return {
showAlert
}
}
You should be able to update showAlert with no problem.
The second error vue/no-async-in-computed-properties, you cannot write async function inside computed(), so the alternative is to use watch instead.