Vue template does not update value (composition api) - vue.js

I have a functional component:
export default defineComponent({
name: 'MovieOverview',
components: {
ExpandedMovieInformation,
},
setup() {
let toggleValue = false;
const toggleExpandedMovieInformation = (moviex: Movie) => {
toggleValue = !toggleValue;
console.log(toggleValue)
};
return {
toggleValue,
toggleExpandedMovieInformation,
};
},
});
<template>
<div>
<button v-on:click='toggleExpandedMovieInformation'>click</button>
{{ toggleValue }}
</div>
</template>
When I click the button the console.log does log the change, but the toggleValue in the template stays the same value: false.

Right now the toggleValue variable has no reactivity. You should use ref() or reactive() in order to make it reactive so the view re-renders every time changes are made into that property.
So you should do something like this:
import { ref } from 'vue'
export default defineComponent({
name: 'MovieOverview',
components: {
ExpandedMovieInformation,
},
setup() {
let toggleValue = ref(false);
const toggleExpandedMovieInformation = (moviex: Movie) => {
// now you'll have to access its value through the `value` property
toggleValue.value = !toggleValue.value;
console.log(toggleValue.value)
};
return {
toggleValue,
toggleExpandedMovieInformation,
};
},
});
<template>
<div>
<button v-on:click='toggleExpandedMovieInformation'>click</button>
<!-- You DON'T need to change toggleValue to toggleValue.value in the template -->
{{ toggleValue }}
</div>
</template>
Check the docs for more info about ref and reactive.

Related

Is there a way to share reactive data between random components in Vue 3 Composition API?

Having some reactive const in "Component A," which may update after some user action, how could this data be imported into another component?
For example:
const MyComponent = {
import { computed, ref } from "vue";
setup() {
name: "Component A",
setup() {
const foo = ref(null);
const updateFoo = computed(() => foo.value = "bar");
return { foo }
}
}
}
Could the updated value of 'foo' be used in another Component without using provide/inject?
I am pretty new in the Vue ecosystem; kind apologies if this is something obvious that I am missing here.
One of the best things about composition API is that we can create reusable logic and use that all across the App. You create a composable functions in which you can create the logic and then import that into the components where you want to use it. Not only does this make your component much cleaner but also your APP much more maintainable. Below is a simple example of counter to show how they can be used. You can find working demo here:
Create a composable function for counter:
import { ref, computed } from "vue";
const counter = ref(0);
export const getCounter = () => {
const incrementCounter = () => counter.value++;
const decrementCounter = () => counter.value--;
const counterPositiveOrNegitive = computed(() =>
counter.value >= 0 ? " Positive" : "Negitive"
);
return {
counter,
incrementCounter,
decrementCounter,
counterPositiveOrNegitive
};
};
Then you can import this function into your components and get the function or you want to use. Component to increment counter.
<template>
<div class="hello">
<h1>Component To Increment Counter</h1>
<button #click="incrementCounter">Increment</button>
</div>
</template>
<script>
import { getCounter } from "../composables/counterExample";
export default {
name: "IncrementCounter",
setup() {
const { incrementCounter } = getCounter();
return { incrementCounter };
},
};
</script>
Component to decrement counter:
<template>
<div class="hello">
<h1>Component To Decrement Counter</h1>
<button #click="decrementCounter">Decrement</button>
</div>
</template>
<script>
import { getCounter } from "../composables/counterExample";
export default {
name: "DecrementCounter",
setup() {
const { decrementCounter } = getCounter();
return { decrementCounter };
},
};
</script>
Then in the main component, you can show the counter value.
<template>
<img alt="Vue logo" src="./assets/logo.png" />
<div class="counters">
<IncrementCounter />
<DecrementCounter />
</div>
<h3>Main Component </h3>
<p>Counter: {{ counter }}</p>
<p>{{ counterPositiveOrNegitive }}</p>
</template>
<script>
import IncrementCounter from "./components/IncrementCounter.vue";
import DecrementCounter from "./components/DecrementCounter.vue";
import { getCounter } from "./composables/counterExample";
export default {
name: "App",
components: {
IncrementCounter: IncrementCounter,
DecrementCounter: DecrementCounter,
},
setup() {
const { counter, counterPositiveOrNegitive } = getCounter();
return { counter, counterPositiveOrNegitive };
},
};
Hope this was somewhat helpful. You can find a working example here:
https://codesandbox.io/s/vue3-composition-api-blfpj

Update reactive object made from prop after prop changes

I have the following component:
<template>
<vote-buttons :score="commentRef.score"
#update-score="updateScore">
</vote-buttons>
</template>
<script>
props: {
comment: {type: Object}
},
setup(props) {
const commentRef = ref(props.comment);
const updateScore = (value) => {
commentRef.value.score = value;
}
}
</script>
Problem is when parent component loops again..
<comment v-for="comment in comments" :comment="comment">
</comment>
then prop has new data, but commentRef is not updated. How to retrigger reactive object creation after prop changes? Thanks
There are some issues in the code:
you should add a key in the parent component: (Here guessing you have an unique id in comment object)
<Comment
v-for="comment in comments"
:comment="comment"
:key="comment.id"></Comment>
Define comments as a reactive object, child no need to ref for the props, below give a similar sample but not exactly the same with your code:
Parent component:
<template>
<Comment v-for="comment in comments" :comment="comment" :key="comment.id"></Comment>
<button #click="addCmt">addCmt</button>
</template>
<script>
import Comment from "./components/Comment";
import {reactive} from "vue";
export default {
name: "Params",
setup() {
const comments = reactive([{id: 1, name: 'a'}, {id: 6, name: 'c'}]);
function addCmt() {
comments.unshift({id: comments.length + 10, name: 'k'});
}
return {
comments,
addCmt
}
},
components: {Comment}
}
</script>
Comment component:
<template>
<div>comments {{ comment.id }}</div>
</template>
<script>
export default {
name: "Comment",
props: ['comment'],
setup(props, ctx) {
const comment = props.comment;
return {comment};
}
}
</script>

How to use vnode in vue template

I want to call the dialog like this:
import demo from './demo.vue';
methods: {
open() {
const dialog = this.$dialog({
content: demo
});
}
}
dialog.js
import Vue from 'vue';
import QfDialog from './qf-dialog';
import ElementQfUI from 'element-qf-ui';
Vue.use(ElementQfUI);
let DialogConstructor = Vue.extend(QfDialog);
export const dialog = (params) => {
const instance = new DialogConstructor({
propsData: {
visible: true,
...params
}
});
instance.$mount();
document.body.appendChild(instance.$el);
return instance;
}
Vue.prototype.$dialog = dialog;
I tried to generate a VNode from a vue object to use in the template, but it gives me following error:
Error in render: "TypeError: Converting circular structure to JSON
--> starting at object with constructor 'Object'
--- property '_renderProxy' closes the circle"
<template>
<el-dialog :visible.sync="visible" v-bind="$attrs" v-on="$listeners">
{{ contentTpl }}
</el-dialog>
</template>
<script>
export default {
name: 'qf-dialog',
props: {
visible: {
type: Boolean,
default: false
}
},
data() {
return {
contentTpl: null
};
},
created() {
// this.content is a vue obj
let content = JSON.parse(JSON.stringify(this.content));
let vnode = this.$createElement('demo-cc', content);
this.contentTpl = [vnode];
}
};
</script>
How do I make {{contentTpl}} work ?
You are passing a whole Vue component into your Dialog - not a VNodes
Just use Dynamic Components
<template>
<el-dialog :visible.sync="visible" v-bind="$attrs" v-on="$listeners">
<component :is="content" />
</el-dialog>
</template>
Note that using :visible.sync is problematic as Vue does not allow to modify props the component receives

VueJS 2.x Child-Component doesn't react to changed parent-property

I have the problem, that a component doesn't recognize the change of a property.
The component is nested about 5 levels deep. Every component above the faulty one does update with the same mechanics and flawlessly.
I invested some time to get to the problem, but I can't find it.
The flow is:
Dashboard (change value and pass as prop)
TicketPreview (Usage and
pass prop)
CommentSection (Pass prop)
CommentList (FAULTY / Usage of prop)
Everything down to the commentSection is being updated as expected, but the commentList doesn't get the update notification (beforeUpdate doesn't get triggered).
Since I tested quite a few things I will only post the essential code from commentSection (parent) and commenList (child)
DISCLAIMER: This is a prototype code without backend, therefore typical API-Requests are solved with the localStorage of the users browser.
commentSection
<template>
<div id="comment-section">
<p>{{selectedTicket.title}}</p>
<comment-form :selectedTicket="selectedTicket" />
<comment-list :selectedTicket="selectedTicket" />
</div>
</template>
<script>
import CommentForm from "#/components/comment-section/CommentForm";
import CommentList from "#/components/comment-section/CommentList";
export default {
name: "CommentSection",
components: {
CommentForm,
CommentList,
},
props: {
selectedTicket: Object,
},
beforeUpdate() {
console.log("Comment Section");
console.log(this.selectedTicket);
},
updated() {
console.log("Comment Section is updated");
}
}
</script>
CommentList
<template>
<div id="comment-list">
<comment-item
v-for="comment in comments"
:key="comment.id"
:comment="comment"
/>
</div>
</template>
<script>
import CommentItem from "#/components/comment-section/CommentItem";
export default {
name: "CommentList",
components: {
CommentItem,
},
data() {
return {
comments: Array,
}
},
props: {
selectedTicket: Object,
},
methods: {
getComments() {
let comments = JSON.parse(window.localStorage.getItem("comments"));
let filteredComments = [];
for(let i = 0; i < comments.length; i++){
if (comments[i].ticketId === this.selectedTicket.id){
filteredComments.push(comments[i]);
}
}
this.comments = filteredComments;
}
},
beforeUpdate() {
console.log("CommentList");
console.log(this.selectedTicket);
this.getComments();
},
mounted() {
this.$root.$on("updateComments", () => {
this.getComments();
});
console.log("CL Mounted");
},
}
</script>
The beforeUpdate() and updated() hooks from the commentList component are not being fired.
I guess I could work around it with an event passing the data, but for the sake of understanding, let's pretend it's not a viable option right now.
It would be better to use a watcher, this will be more simple.
Instead of method to set comments by filtering you can use computed property which is reactive and no need to watch for props updates.
CommentSection
<template>
<div id="comment-section">
<p>{{ selectedTicket.title }}</p>
<comment-form :selectedTicket="selectedTicket" />
<comment-list :selectedTicket="selectedTicket" />
</div>
</template>
<script>
import CommentForm from "#/components/comment-section/CommentForm";
import CommentList from "#/components/comment-section/CommentList";
export default {
name: "CommentSection",
components: {
CommentForm,
CommentList
},
props: {
selectedTicket: Object
},
methods: {
updateTicket() {
console.log("Comment section is updated");
console.log(this.selectedTicket);
}
},
watch: {
selectedTicket: {
immediate: true,
handler: "updateTicket"
}
}
};
</script>
CommentList
<template>
<div id="comment-list">
<comment-item
v-for="comment in comments"
:key="comment.id"
:comment="comment"
/>
</div>
</template>
<script>
import CommentItem from "#/components/comment-section/CommentItem";
export default {
name: "CommentList",
components: {
CommentItem
},
props: {
selectedTicket: Object
},
computed: {
comments() {
let comments = JSON.parse(window.localStorage.getItem("comments"));
let filteredComments = [];
for (let comment of comments) {
if (comment.ticketId == this.selectedTicket.id) {
filteredComments.push(comment);
}
}
// // using es6 Array.filter()
// let filteredComments = comments.filter(
// (comment) => comment.ticketId == this.selectedTicket.id
// );
return filteredComments;
}
}
};
</script>
I found the problem: Since commentList is only a wrapper that doesn't use any of the values from the prop, the hooks for beforeUpdate and updated are never triggered. The Vue Instance Chart is misleading in that regard. The diagram shows it like beforeUpdate would ALWAYS fire, when the data changed (then re-render, then updated), but beforeUpdate only fires if the Component and Parent has to be re-rendered.
The Object updates as expected, it just never triggered a re-render on the child component because the wrapper has not been re-rendered.

Element UI dialog component can open for the first time, but it can't open for the second time

I'm building web app with Vue, Nuxt, and Element UI.
I have a problem with the Element dialog component.
It can open for the first time, but it can't open for the second time.
This is the GIF about my problem.
https://gyazo.com/dfca3db76c75dceddccade632feb808f
This is my code.
index.vue
<template>
<div>
<el-button type="text" #click="handleDialogVisible">click to open the Dialog</el-button>
<modal-first :visible=visible></modal-first>
</div>
</template>
<script>
import ModalFirst from './../components/ModalFirst.vue'
export default {
components: {
'modal-first': ModalFirst
},
data() {
return {
visible: false,
};
},
methods: {
handleDialogVisible() {
this.visible = true;
}
}
}
</script>
ModalFirst.vue
<template>
<el-dialog
title="Tips"
:visible.sync="visible"
width="30%"
>
<span>This is a message</span>
<span slot="footer" class="dialog-footer">
<a>Hello</a>
</span>
</el-dialog>
</template>
<script>
export default {
props: [ 'visible' ]
}
</script>
And I can see a warning message on google chrome console after closing the dialog.
The warning message is below.
webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js:620 [Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "visible"
found in
---> <ModalFirst> at components/ModalFirst.vue
<Pages/index.vue> at pages/index.vue
<Nuxt>
<Layouts/default.vue> at layouts/default.vue
<Root>
This is the screenshot of the warning message.
https://gyazo.com/83c5f7c5a8e4d6816c35b3116c80db0d
In vue , using directly to prop value is not allowed . Especially when your child component will update that prop value , in my option if prop will be use
for display only using directly is not a problem .
In your code , .sync will update syncronously update data so I recommend to create local data.
ModalFirst.vue
<el-dialog
title="Tips"
:visible.sync="localVisible"
width="30%"
>
<script>
export default {
props: [ 'visible' ],
data: function () {
return {
localVisible: this.visible // create local data using prop value
}
}
}
</script>
If you need the parent visible property to be updated, you can create your component to leverage v-model:
ModalFirst.vue
<el-dialog
title="Tips"
:visible.sync="localVisible"
width="30%"
>
<script>
export default {
props: [ 'value' ],
data() {
return {
localVisible: null
}
},
created() {
this.localVisible = this.value;
this.$watch('localVisible', (value, oldValue) => {
if(value !== oldValue) { // Optional
this.$emit('input', value); // Required
}
});
}
}
</script>
index.vue
<template>
<div>
<el-button type="text" #click="handleDialogVisible">click to open the Dialog</el-button>
<modal-first v-model="visible"></modal-first>
</div>
</template>
<script>
import ModalFirst from './../components/ModalFirst.vue'
export default {
components: {
'modal-first': ModalFirst
},
data() {
return {
visible: false,
};
},
methods: {
handleDialogVisible() {
this.visible = true;
}
}
}
</script>
v-model is basically a shorthand for :value and #input
https://v2.vuejs.org/v2/guide/forms.html#Basic-Usage
Side-note:
You can also import your component like so:
components: { ModalFirst },
as ModalFirst will be interpreted as modal-first as well by Vue.js