Vue3: changing a normal object causes rerender - vue.js

In the following code state is just a normal object but changing its prop: message (again, just a normal prop) causes a rerender. Why?
const App = {
setup() {
const name = Vue.ref("");
Vue.watch(name, () => state.message = `Hello ${name.value}`);
const state = {
name,
message: "Welcome stranger"
};
return state;
}
};
Vue.createApp(App).mount("#root");
<script src="https://unpkg.com/vue#next"></script>
<div id="root">
name: <input v-model="name" /> <br/> message: {{ message }}
</div>

As #MichalLevý pointed out, Vue doesn't really watch or rerender on state.message change. The change in message is only reflected in the DOM because of batching changes in response to name change.
This can be shown by just changing state.message in a timer handler. There's no rerender.
const App = {
setup() {
const name = Vue.ref("");
Vue.watch(name, () => state.message = `Hello ${name.value}`);
setTimeout(() => {
state.message = "only message changed";
}, 2000);
const state = {
name,
message: "Welcome stranger"
};
return state;
}
};
Vue.createApp(App).mount("#root");
<script src="https://unpkg.com/vue#next"></script>
<div id="root">
name: <input v-model="name" /> <br/> message: {{ message }}
</div>

Related

Can't get events from emits in my composable

I'm trying to generate a confirmation modal when calling my composable, my component instance is mounting well, but I can't access the emits via the : onCancel
The goal is to call the dialer every time I need to interact with a confirmation
useModalConfirm.ts
function confirm(props: ModalConfirmProps) {
const container = document.createElement('div');
document.body.appendChild(container);
const component = createVNode(ModalConfirm, {
...props,
// not working here :(
onCancel: () => {
console.log('canceled')
}
});
render(component, container);
return component.component;
}
ModalConfirm.vue
<script lang="ts" setup>
import {NButton} from "naive-ui";
const emits = defineEmits(["onConfirm", "onCancel"]);
export type ModalConfirmProps = {
title: string;
message: string;
confirmButtonText: string,
cancelButtonText: string,
};
const props = defineProps<ModalConfirmProps>();
const confirm = () => {
emits("onConfirm");
};
const cancel = () => {
emits("onCancel");
};
</script>
<template>
<div class="ModalConfirm">
<div class="ModalConfirmContent">
{{ props.title }}
{{ props.message }}
<NButton #click="cancel" type="error">{{ props.cancelButtonText }}</NButton>
<NButton #click="confirm" type="success">{{ props.confirmButtonText }}</NButton>
</div>
</div>
</template>
any ideas ?

Uncaught (in promise) TypeError: Cannot read properties of null (reading 'name') in vue 3

I'm completely new to vue and is following this simple example about real time socket message, but i got the
error as the title mentioned, after i enter the name, when i type in the message and press send, it return error. it's in this 2 lines
<div v-for="message in messages" :key="message">
[{{ message.name }}]: {{ message.text }}
my full source of app.vue
<script setup>
import { io } from 'socket.io-client'
import { onBeforeMount, ref } from 'vue';
const socket = io('http://localhost:3000');
const messages = ref([]);
const messageText = ref('');
const joined = ref(false);
const name = ref('');
const typingDisplay = ref('');
onBeforeMount(() => {
socket.emit('findAllMessages', {}, (response) => {
messages.value = response;
});
socket.on('message', (message) => {
messages.value.push(message);
});
socket.on('typing', ({name, isTyping}) => {
if (isTyping) {
typingDisplay.value = `${name} is typing...`;
} else {
typingDisplay.value = '';
}
});
});
const join = () => {
socket.emit('join', {name: name.value}, () => {
joined.value = true;
});
};
const sendMessage = () => {
socket.emit('createMessage', { text: messageText.value}, () => {
messageText.value = '';
});
};
let timeout;
const emitTyping = () => {
socket.emit('typing', {isTyping: true});
timeout = setTimeout(() => {
socket.emit('typing', {isTyping: false});
}, 2000);
};
</script>
<template>
<div class="chat">
<div v-if="!joined">
<form #submit.prevent="join">
<label>What's your name?</label>
<input v-model="name" />
<button type="submit">Send</button>
</form>
</div>
<div class="chat-container" v-else>
<div class="messages-container">
<div v-for="message in messages" :key="message">
[{{ message.name }}]: {{ message.text }}
</div>
</div>
<div v-if="typingDisplay">{{ typingDisplay}}</div>
<hr/>
<div class="message-input">
<form #submit.prevent="sendMessage">
<label>Message:</label>
<input v-model="messageText" #input="emitTyping" />
<button type="submit">Send</button>
</form>
</div>
</div>
</div>
</template>
<style >
#import './assets/base.css';
.chat{
padding:20px;
height: 100vh;
}
.chat-container{
display: flex;
flex-direction: column;
height: 100%;
}
.message-container{
flex: 1;
}
</style>
I can't see anything wrong with the code itself. Though the key of an element should generally not be assigned an object, but I doubt this would cause the issue you mention.
I assume either the code is not handling the message object correctly, because of which the 'name' property is not filled, or the object is being read in the HTML before it's actually filled. I would suggest further console logging in the process prior to the HTML render to see if the object is being filled with the appropriate data.

Watch, Compare & post updated form data to API using Axios in Vue 3

I need help to complete my code.
This is what have done.
I am fetching options from API, so I have defined the initial state as
empty.
Once I have a response from API, I update the state of options.
My form is displayed once I have a response from API.
Now using v-bind I am binding the form.
Where I need help.
I need to watch for the changes in form. If the values of form elements are different from the state of the API response, I would like to enable the submit button.
When the save button is clicked, I need to filter the options that were changed & submit that form data to my pinia action called updateOptions.
Note: API handles post data in this way. Example: enable_quick_view: true
Thank you in advance.
options.js pinia store
import { defineStore } from 'pinia'
import Axios from 'axios';
import axios from 'axios';
const BASE_API_URL = adfy_wp_locolizer.api_url;
export const useOptionsStore = defineStore({
id: 'Options',
state: () => ({
allData: {},
options: {
enable_quick_view: null, // boolean
quick_view_btn_label: "", // string
quick_view_btn_position: "", // string
},
newOptions: {}, // If required, holds the new options to be saved.
message: "", // Holds the message to be displayed to the user.
isLoading: true,
isSaving: false,
needSave: false,
errors: [],
}),
getters: {
// ⚡️ Return state of the options.
loading: (state) => {
return state.isLoading;
},
},
actions: {
// ⚡️ Use Axios to get options from api.
fetchOptions() {
Axios.get(BASE_API_URL + 'get_options')
.then(res => {
this.alldata = res.data.settings;
let settings = res.data.settings_values;
/*
* Set options state.
*/
this.options.enable_quick_view = JSON.parse(
settings.enable_quick_view
);
this.options.quick_view_btn_label =
settings.quick_view_btn_label;
this.options.quick_view_btn_position = settings.quick_view_btn_position;
/*
* End!
*/
this.isLoading = false;
})
.catch(err => {
this.errors = err;
console.log(err);
})
.finally(() => {
// Do nothing for now.
});
},
// ⚡️ Update options using Axios.
updateOptions() {
this.isSaving = true;
axios.post(BASE_API_URL + 'update_options', payload)
.then(res => {
this.needSave = false;
this.isSaving = false;
this.message = "Options saved successfully!";
})
.catch(err => {
this.errors = err;
console.log(err);
this.message = "Error saving options!";
})
}
},
});
Option.vue component
<script setup>
import { onMounted, watch } from "vue";
import { storeToRefs } from "pinia";
import { Check, Close } from "#element-plus/icons-vue";
import Loading from "../Loading.vue";
import { useOptionsStore } from "../../stores/options";
let store = useOptionsStore();
let { needSave, loading, options, newOptions } = storeToRefs(store);
watch(
options,
(state) => {
console.log(state);
// Assign the option to the newOptions.
},
{ deep: true, immediate: false }
);
onMounted(() => {
store.fetchOptions();
});
</script>
<template>
<Loading v-if="loading" />
<form
v-else
id="ui-settings-form"
class="ui-form"
#submit="store.updateOptions()"
>
<h3 class="option-box-title">General</h3>
<div class="ui-options">
<div class="ui-option-columns option-box">
<div class="ui-col left">
<div class="label">
<p class="option-label">Enable quick view</p>
<p class="option-description">
Once enabled, it will be visible in product catalog.
</p>
</div>
</div>
<div class="ui-col right">
<div class="input">
<el-switch
v-model="options.enable_quick_view"
size="large"
inline-prompt
:active-icon="Check"
:inactive-icon="Close"
/>
</div>
</div>
</div>
</div>
<!-- // ui-options -->
<div class="ui-options">
<div class="ui-option-columns option-box">
<div class="ui-col left">
<div class="label">
<p class="option-label">Button label</p>
</div>
</div>
<div class="ui-col right">
<div class="input">
<el-input
v-model="options.quick_view_btn_label"
size="large"
placeholder="Quick view"
/>
</div>
</div>
</div>
</div>
<!-- // ui-options -->
<button type="submit" class="ui-button" :disabled="needSave == true">
Save
</button>
</form>
</template>
<style lang="css" scoped>
.el-checkbox {
--el-checkbox-font-weight: normal;
}
.el-select-dropdown__item.selected {
font-weight: normal;
}
</style>
In the watch function you can compare the new and old values. But you shuld change it to:
watch(options, (newValue, oldValue) => {
console.log(oldValue, newValue);
// compare objects
}, {deep: true, immediate: false};
Now you can compare the old with the new object. I think search on google can help you with that.
Hope this helps.

VueJS test-utils can't find element inside child component

I'm trying to use findComponent with find method to find a child component's element and set it's value. But every time I run test, it gives me Cannot call setValue on an empty DOMWrapper. error.
Test file
import { mount } from '#vue/test-utils';
import Create from './Create.vue';
// import State from '#/components/State.vue';
describe('it tests Create component', () => {
test('it emits create event and resets the form when form is valid and create button is clicked', async () => {
const div = document.createElement('div');
div.id = 'root';
document.body.append(div);
const expectedNameValue = 'TODO_NAME';
const expectedStateValue = 'Pending';
const wrapper = mount(Create, {
attachTo: '#root',
});
await wrapper.find(`input`).setValue(expectedNameValue);
await wrapper.findComponent({ ref: 'state-component' }).find('select').setValue(expectedStateValue);
await wrapper.find(`form`).trigger('submit');
expect(wrapper.emitted().create).toBeTruthy();
expect(wrapper.emitted().create[0]).toEqual([expectedNameValue]);
expect(wrapper.emitted().create[1]).toEqual(['Pending']);
expect(wrapper.find(`input[name='name']`).element.value).toEqual('');
expect(wrapper.find(`input[name='state']`).element.value).toEqual('Pending');
});
});
Create component
<template>
<form #submit.prevent="createTodo" class="flex gap-2 w-full">
<input class="flex-1 shadow rounded-md p-2 focus:ring-2 focus:ring-blue-900 focus:outline-none" type="text" placeholder="Todo Name" name="name" required/>
<State ref="state-component"/>
<button type="submit" class="rounded-md shadow text-white bg-blue-700 py-2 px-6">Create</button>
</form>
</template>
<script>
import State from '#/components/State.vue';
export default {
components: { State },
emits: ['create'],
methods: {
createTodo(event) {
const elems = event.target.elements;
const todo = { name: elems.name.value, state: elems.state.value };
this.$emit('create', todo);
elems.name.value = '';
elems.state.value = 'Pending';
}
}
}
</script>
<style scoped>
</style>
State component
<template>
<select id="state-select" class="rounded-md bg-green-200 text-white" name="state">
<option
v-for="(state, index) in states"
:selected="isSelected(state)"
:key="index"
>
{{ state }}
</option>
</select>
</template>
<script>
export default {
props: ["todo", "index"],
data() {
return {
name: "",
state: "",
states: ["Pending", "In Progress", "Done"],
};
},
created() {
if(!this.todo) return true;
this.state = this.todo.state;
this.name = this.todo.name;
},
methods: {
isSelected(equivalent){
return equivalent === this.state;
}
}
};
</script>
<style scoped></style>
I'm fairly new to VueJS so I'm open to all tips and tricks, thanks.
Some issues to fix:
You don't need to attach the component to the document, so remove that:
// ❌
// const div = document.createElement('div');
// div.id = 'root';
// document.body.append(div);
// const wrapper = mount(Create, { attachTo: '#root' });
// ✅
const wrapper = mount(Create);
The template ref to the State component would be the component's root element, so no need to find() the <select>:
// ❌
// await wrapper.findComponent({ ref: 'state-component' }).find('select').setValue(expectedStateValue);
^^^^^^^^^^^^^^^
// ✅
await wrapper.findComponent({ ref: 'state-component' }).setValue(expectedStateValue);
The emitted() object key is the event name, and the value is an array of of arrays, containing emitted data. You can verify the first create-event data contains another object with toMatchObject(object):
// ❌
// expect(wrapper.emitted().create[0]).toEqual([expectedNameValue]);
// expect(wrapper.emitted().create[1]).toEqual(['Pending']);
// ✅
expect(wrapper.emitted().create[0][0]).toMatchObject({ name: expectedNameValue, state: 'Pending' });
The last assertion tries to find input[name='state'], but that's actually a <select>, not an <input>:
// ❌
// expect(wrapper.find(`input[name='state']`).element.value).toEqual('Pending')
^^^^^
// ✅
expect(wrapper.find(`select[name='state']`).element.value).toEqual('Pending')
demo

Vue 3: Why changing a prop of a watched object in the watch handler doesn't create infinite loop?

I'd expect that this code would create an infinite loop since the object that is watched is changed in the watch handler:
const App = {
setup() {
const state = Vue.reactive({
name: "",
message: "Welcome stranger"
});
Vue.watch(state, () => state.message = `Hello ${state.name}`);
return state;
}
};
Vue.createApp(App).mount("#root");
<script src="https://unpkg.com/vue#next"></script>
<div id="root">
name: <input v-model="name"/> <br/>
message: {{ message }}
</div>
But it actually works as desired. I'm really surprised.
Does doing it like this have some performance penalty or is it a completely valid code?
As #slauth pointed out this works because Vue checks if the changed value is different that the previous and doesn't trigger updates if the value didn't change. This can be shown by logging each time the handler is fired:
const App = {
setup() {
const state = Vue.reactive({
name: "",
message: "Welcome stranger"
});
Vue.watch(state, () => {
console.log("watch handler fired for", state.name);
state.message = `Hello ${state.name}`;
});
return state;
}
};
Vue.createApp(App).mount("#root");
<script src="https://unpkg.com/vue#next"></script>
<div id="root">
name: <input v-model="name" /> <br/> message: {{ message }}
</div>
This means that the watch handler is called twice for each dependency change. So although it works it has a performance cost.