Variable changed in parent but is not changed in child VueJs - vue.js

I try to change a Boolean variable isSubmited after a post request is made from parent component, and this will trigger a watch from child and there will be another post request that uses the ID form first response.
The flow begins from parent component.
User click on submit
In parent I create a new contract and I receive an ID from response (after post request is made)
After post request, I change a Boolean value ,to activate a watch from child.
In child component I will do another post that uses the ID form parent response
Basically 2 post request, first in parent and the second one in child that uses the ID from parent.
From parent component
<upload-file-test :id_contract="id_contract" :isSubmited="isSubmited" ></upload-file-test>
addContract(data) {
this.isSubmited = !this.isSubmited; //from here in child component watch it works
post("/url", data)
.then((response) => {
this.id_contract = response.data;
this.isSubmited = !this.isSubmited; //from here in child component is not changed
}
}
From child component
props: ['id_contract', 'isSubmited']
watch:{
isSubmited: {
handler(){
console.log('Submited');
//here will be a method for post request
}
},
}

As you can see in the example below the fact that changing a variable in a promise trigger correctly the watcher.
I think that that your post is failing and thus not actually changing isSubmited.
You should catch the error.
post("/url", data).then(response => {
this.id_contract = response.data;
this.isSubmited = !this.isSubmited;
}).catch(error => {
console.log("post is failing")
});
Vue.config.devtools = false;
Vue.config.productionTip = false;
var app = new Vue({
el: '#app',
data: {
bool: false
},
methods: {
test() {
this.bool = !this.bool;
fetch("https://jsonplaceholder.typicode.com/todos/1").then(() => {
this.bool = !this.bool;
});
}
},
watch: {
bool: {
handler() {
console.log("CHANGE");
}
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<button #click="test">test</button>
</div>

Related

How to call function when update state and after DOM full loaded

I want when update state and after DOM full loaded, I will use js to update CSS. So now, I'm using document ready function in the method. Is there any writing style in Vuex? How can I write them in mounted?
computed: {
...mapGetters([
'wsInfo'
])
},
mounted () {
??????
},
method: {
moveWs (from, to) {
//update state
this.wsInfo.workspaces.splice(to, 0, this.wsInfo.workspaces.splice(from, 1)[0])
$(document).ready(function () {
// code run after update state and dom loaded
})
}
}
You need to use a nextTick Function.
Also you can use it inside a method vuejs object.
You can read more about it here:
https://v2.vuejs.org/v2/api/#vm-nextTick
new Vue({
// ...
methods: {
// ...
example: function () {
// modify data
this.message = 'changed'
// DOM is not updated yet
this.$nextTick(function () {
// DOM is now updated
// `this` is bound to the current instance
this.doSomethingElse()
})
}
}
})

When passing data from parent component to child component via props, the data appears to be undefined in the mounted hook of the child component

In my parent component:
<UsersList :current-room="current_room" />
In the child component:
export default {
props: {
currentRoom: Object
},
data () {
return {
users: []
}
},
mounted () {
this.$nextTick( async () => {
console.log(this.currentRoom) // this, weirdly, has the data I expect, and id is set to 1
let url = `${process.env.VUE_APP_API_URL}/chat_room/${this.currentRoom.id}/users`
console.log(url) // the result: /api/chat_room/undefined/users
let response = await this.axios.get(url)
this.users = response.data
})
},
}
When I look at the page using vue-devtools, I can see the data appears:
I've run into this issue in the past – as have many others. For whatever reason, you can't rely on props being available in the component's mounted handler. I think it has to do with the point at which mounted() is called within Vue's lifecycle.
I solved my problem by watching the prop and moving my logic from mounted to the watch handler. In your case, you could watch the currentRoom property, and make your api call in the handler:
export default {
props: {
currentRoom: Object
},
data() {
return {
users: []
}
},
watch: {
currentRoom(room) {
this.$nextTick(async() => {
let url = `${process.env.VUE_APP_API_URL}/chat_room/${room.id}/users`
let response = await this.axios.get(url)
this.users = response.data
})
}
},
}
I don't think you really need to use $nextTick() here, but I left it as you had it. You could try taking that out to simplify the code.
By the way, the reason console.log(this.currentRoom); shows you the room ID is because when you pass an object to console.log(), it binds to that object until it is read. So even though the room ID is not available when console.log() is called, it becomes available before you see the result in the console.

Open a VueJS component on a new window

I have a basic VueJS application with only one page.
It's not a SPA, and I do not use vue-router.
I would like to implement a button that when clicked executes the window.open() function with content from one of my Vue Components.
Looking at the documentation from window.open() I saw the following statement for URL:
URL accepts a path or URL to an HTML page, image file, or any other resource which is supported by the browser.
Is it possible to pass a component as an argument for window.open()?
I was able to use some insights from an article about Portals in React to create a Vue component which is able to mount its children in a new window, while preserving reactivity! It's as simple as:
<window-portal>
I appear in a new window!
</window-portal>
Try it in this codesandbox!
The code for this component is as follows:
<template>
<div v-if="open">
<slot />
</div>
</template>
<script>
export default {
name: 'window-portal',
props: {
open: {
type: Boolean,
default: false,
}
},
data() {
return {
windowRef: null,
}
},
watch: {
open(newOpen) {
if(newOpen) {
this.openPortal();
} else {
this.closePortal();
}
}
},
methods: {
openPortal() {
this.windowRef = window.open("", "", "width=600,height=400,left=200,top=200");
this.windowRef.addEventListener('beforeunload', this.closePortal);
// magic!
this.windowRef.document.body.appendChild(this.$el);
},
closePortal() {
if(this.windowRef) {
this.windowRef.close();
this.windowRef = null;
this.$emit('close');
}
}
},
mounted() {
if(this.open) {
this.openPortal();
}
},
beforeDestroy() {
if (this.windowRef) {
this.closePortal();
}
}
}
</script>
The key is the line this.windowRef.document.body.appendChild(this.$el); this line effectively removes the DOM element associated with the Vue component (the top-level <div>) from the parent window and inserts it into the body of the child window. Since this element is the same reference as the one Vue would normally update, just in a different place, everything Just Works - Vue continues to update the element in response to databinding changes, despite it being mounted in a new window. I was actually quite surprised at how simple this was!
You cannot pass a Vue component, because window.open doesn't know about Vue. What you can do, however, is to create a route which displays your component and pass this route's URL to window.open, giving you a new window with your component. Communication between the components in different windows might get tricky though.
For example, if your main vue is declared like so
var app = new Vue({...});
If you only need to render a few pieces of data in the new window, you could just reference the data model from the parent window.
var app1 = window.opener.app;
var title = app.title;
var h1 = document.createElement("H1");
h1.innerHTML = title;
document.body.appendChild(h1);
I ported the Alex contribution to Composition API and works pretty well.
The only annoyance is that the created window ignores size and position, maybe because it is launched from a Chrome application that is fullscreen. Any idea?
<script setup lang="ts">
import {ref, onMounted, onBeforeUnmount, watch, nextTick} from "vue";
const props = defineProps<{modelValue: boolean;}>();
const emit = defineEmits(["update:modelValue"]);
let windowRef: Window | null = null;
const portal = ref(null);
const copyStyles = (sourceDoc: Document, targetDoc: Document): void => {
// eslint-disable-next-line unicorn/prefer-spread
for(const styleSheet of Array.from(sourceDoc.styleSheets)) {
if(styleSheet.cssRules) {
// for <style> elements
const nwStyleElement = sourceDoc.createElement("style");
// eslint-disable-next-line unicorn/prefer-spread
for(const cssRule of Array.from(styleSheet.cssRules)) {
// write the text of each rule into the body of the style element
nwStyleElement.append(sourceDoc.createTextNode(cssRule.cssText));
}
targetDoc.head.append(nwStyleElement);
}
else if(styleSheet.href) {
// for <link> elements loading CSS from a URL
const nwLinkElement = sourceDoc.createElement("link");
nwLinkElement.rel = "stylesheet";
nwLinkElement.href = styleSheet.href;
targetDoc.head.append(nwLinkElement);
}
}
};
const openPortal = (): void => {
nextTick().then((): void => {
windowRef = window.open("", "", "width=600,height=400,left=200,top=200");
if(!windowRef || !portal.value) return;
windowRef.document.body.append(portal.value);
copyStyles(window.document, windowRef.document);
windowRef.addEventListener("beforeunload", closePortal);
})
.catch((error: Error) => console.error("Cannot instantiate portal", error.message));
};
const closePortal = (): void => {
if(windowRef) {
windowRef.close();
windowRef = null;
emit("update:modelValue", false);
}
};
watch(props, () => {
if(props.modelValue) {
openPortal();
}
else {
closePortal();
}
});
onMounted(() => {
if(props.modelValue) {
openPortal();
}
});
onBeforeUnmount(() => {
if(windowRef) {
closePortal();
}
});
</script>
<template>
<div v-if="props.modelValue" ref="portal">
<slot />
</div>
</template>

Vue pass data from child component to child component

So I have a structure like this
<Root>
<HomeNav> router-view
<RouterLink>
<RouterLink>
<RouterLink>
<RouterLink>
<RouterLink>
<Home> router-view
<RouterLink>
Now, each of HomeNav router links is changing the data in component by passing data to root, however that means that I need to bind that data:
<router-view v-bind:title="title" v-bind:text="text" v-bind:youtube="youtube" v-bind:transition="transition"></router-view>
and run the functions on created and updated:
new Vue({
el: '#app',
router,
data: Variables,
created: function () {
path = pathname.pathname();
pathLenght = pathname.countPathLenght(path);
this.homeText(this.data);
},
updated: function () {
path = pathname.pathname();
pathLenght = pathname.countPathLenght(path);
Pace.restart()
this.homeText(this.data);
},
methods: {
homeText(data) {
data = toogleData.checkText(data);
this.title = data[0];
this.text = data[1];
this.youtube = data[2];
}
}
})
However, the problem is I don't need that data on all of the routes and I don't need to trigger this.homeText function or bind that specific data on every single root. It is only needed on homepage, so the first route.
So the question is, is it possible to directly pass data from HomeNav component to Home component without having all that code in global (root) component?
This is a great place for the MessagePump that the VueJs documentation proposes. It is in essence and unbound Vue object that acts as an intermediary between objects. This allows you to define and call events on the pump which gets passed to the appropriate component.
window.MessagePump = new Vue({});
Vue.Component(
'HomeNav',
{
...
data: function () {
return {
homeText: 'something'
}
},
...
mounted: function () {
var thisArg = this
MessagePump.$on(
'homeTextChanged',
function(newText) {
thisArg.homeText = newText;
}
);
}
...
}
);
Vue.Component(
'Home',
{
...
mounted: function () {
MessagePump.$emit('homeTextChanged', 'To This');
}
...
}
);
This will trigger the event and the changing of homeText from 'something' to 'To This'.

How do I emit an event after dispatch in VueJS?

I have a parent component with a grid of rows and I am using vuex to pass data, as i add a new record I would want to open the newly added record in a modal window. I am emitting an event and opening the modal with the first record (index = 0). The problem is that, as it is an ajax call to add record using an vuex "action", the emit happens before the record is added. How can I wait till the record is added with event? or Is there a better way to do it?
Below is the code:
<!-- Grid Component -->
<grid>
<addrow #newrecordadded="openNewlyAddedRecord"></addrow>
<gridrow v-for="row in rows" :key="row._id"></gridrow>
</grid>
computed: {
rows(){
return this.$store.state.rows;
}
},
methods:{
openNewlyAddedRecord(){
this.openmodal(this.rows[0])
}
}
my store.js
state: {
rows : []
}
so in addrow component once the user clicks on submit form it dispatches to vuex
methods:{
onsubmit(){
this.$store.dispatch('addrow',this.newrow);
this.$emit("newrecordadded");
}
}
actions.js
addrow({ commit,state }, payload){
var url = "/add";
Axios.post(url,payload)
.then(function(response){
commit('addnewrow',response.data);
});
}
mutations.js
addnewrow(state,payload){
state.rows.unshift(payload);
}
Or alternatively can we pass a call back function to dispatch?
You can make the addrow action return a promise, like so:
addrow({ commit,state }, payload){
var url = "/add";
return Axios.post(url,payload)
.then(function(response){
commit('addnewrow',response.data);
});
}
In the onsubmit method in your component you can now do:
this.$store.dispatch('addrow',this.newrow).then(() => this.$emit("newrecordadded");)