I'm trying to make the counter data be transmitted after clicking in the setup, but it doesn't work, what's wrong? Thanks.
setup(props) {
let count = ref(1)
let obj = reactive({ count })
let form = reactive({
name: props.user.name,
count: obj.count,
})
return { form, count, obj }
I change count by:
#click="count++"
And after click data in setup(props) dont update, BUT if i write like as is, it has been reactive:
setup(props) {
let count = ref(1)
let obj = reactive({ count })
let form = reactive({
name: props.user.name,
count: obj,
})
return { form, count, obj }
And after click count: obj will be update, but as it should be the data is passed as an object:
{"count":3}
Why if I try to use obj.count doesn't work dynamically?
TL;DR;
it's because when you access the ref from inside the reactive to assign it to a value it gets the value only, and the reactivity is lost.
here is an example
<script>
import { ref, reactive } from 'vue';
export default{
setup(props) {
let count = ref(1)
let obj = reactive({ count })
let form = reactive({
count: obj.count,
obj: obj
})
return { form, count, obj }
}
}
</script>
<template>
<h1 #click="count++">{{ count }}</h1>
<h1 #click="obj.count++">{{ obj }}</h1>
<h1 #click="form.count++">{{ form }}</h1>
</template>
If you click on #click="count++", the count, obj.count and form.obj.count are updated, but the form.count isn't.
It may not be immediately clear, but when you assign obj.count to form.count, it is not the ref that is passed. Instead the value is resolved, and that is assigned to the value and the reactivity is lost. You can look into the advanced reactivity for some other options. For example, if you use shallowReactive like: let obj = shallowReactive({ count: count }), then the count will remain reactive, but you will need to update the shallowRef with obj.count.value++ instead of obj.count++
example 2
<script>
import { ref, reactive, shallowRef, shallowReactive } from 'vue';
export default{
setup(props) {
let count = ref(1);
let obj = shallowReactive({ count: count });
let form = reactive({
count: obj.count,
obj: obj
});
return { form, count, obj };
}
}
</script>
<template>
<h1 #click="count++">{{ count }}</h1>
<h1 #click="obj.count.value++">{{ obj }}</h1>
<h1 #click="form.count++">{{ form }}</h1>
</template>
Related
I am trying to pass a value into a child component. The child component will then preform the save operation. The parent doesn't need to know anything about it. I am able to pass in the object but not save its updated form.
Parent
<template>
<div v-show="isOpened">
<EditModal #toggle="closeModal" #update:todo="submitUpdate($event)"
:updatedText="editText" :todo="modalPost" />
</div>
</template>
<script setup lang="ts">
import Post from "../components/Post.vue";
import { api } from "../lib/api";
import { ref } from "vue";
import { onMounted } from "vue-demi";
import EditModal from "../components/EditModal.vue";
const postArr = ref('');
const message = ref('');
let isOpened = ref(false);
let modalPost = ref('');
let editText = ref('');
function closeModal() {
isOpened.value = false
}
function openModal(value: string) {
isOpened.value = true
modalPost.value = value
}
// call posts so the table loads updated item
function submitUpdate(value: any) {
console.log("called update in parent " + value)
editText.value = value
posts()
}
</script>
Child EditModal
<template>
<div>
<div>
<textarea id="updateTextArea" rows="10" :value="props.todo.post"></textarea>
</div>
<!-- Modal footer -->
<div>
<button data-modal-toggle="defaultModal" type="button"
#click="update(props.todo.blogId, props.todo.post)">Save</button>
</div>
</div>
</template>
<script lang="ts" setup>
import { api } from "../lib/api";
import { reactive, ref } from "vue";
const props = defineProps({
todo: String,
updatedText: String,
})
const emit = defineEmits(
['toggle','update:todo']
);
function setIsOpened(value: boolean) {
emit('toggle', value);
}
function update(id: string, value: string) {
console.log('the value ' + value)
try {
api.updateBlog(id, value).then( res => {
emit('update:todo', value)
emit('toggle', false);
})
} catch (e) {
console.log('Error while updating post: '+ e)
}
}
</script>
I know the props are read only, therefore I tried to copy it I can only have one model.
I do not see the reason I should $emit to the parent and pass something to another variable to pass back to the child.
I am trying to pass in text to a modal component where it can edit the text and the child component saves it.
Advice?
First, your component should be named EditTodo no EditModal because you are not editing modal. All Edit components should rewrite props to new local variables like ref or reactive, so you can work on them, they won't be read only any more.
Child EditTodo.vue
<template>
<div>
<div>
<textarea id="updateTextArea" rows="10" v-model="data.todo.title"></textarea>
</div>
<div>
<button data-modal-toggle="defaultModal" type="button" #click="update()">Save</button>
</div>
</div>
</template>
<script setup lang="ts">
import { api } from "../lib/api";
import { reactive } from "vue";
const props = defineProps<{ id: number, todo: { title: string} }>()
const emit = defineEmits(['toggle']);
const data = reactive({ ...props })
// I assuming api.updateBlog will update data in database
// so job here should be done just need toogle false modal
// Your array with todos might be not updated but here is
// no place to do that. Well i dont know how your API works.
// Database i use will automaticly update arrays i updated objects
function update() {
try {
api.updateBlog(data.id, data.todo ).then(res => {
emit('toggle', false);
})
}
}
</script>
Parent
<template>
<div>
<BaseModal v-if="todo" :show="showModal">
<EditTodo :id="todo.id" :todo="todo.todo" #toggle="(value) => showModal = value"></EditTodo>
</BaseModal>
</div>
</template>
<script setup lang="ts">
const showModal = ref(false)
const todo = reactive({ id: 5, todo: { title: "Todo number 5"} })
</script>
I separated a modal object with edit form, so you can create more forms and use same modal. And here is a simple, not fully functional modal.
<template>
<div class="..."><slot></slot></div>
</template>
<script setup>
defineProps(['show'])
defineEmits(['toogle'])
</script>
You might want to close modal when user click somewhere outside of modal.
I have an audio player component that needs to send a message to all other players to pause themselves when it is clicked. However, there's a variable amount of components all inside of a v-for loop. It looks something like this:
<template>
<AudioPlayer v-for="song in songs" :key="song.id" #play="pauseOthers" />
</template>
<script>
import { defineComponent, ref } from 'vue';
import AudioPlayer from '#/components/AudioPlayer.vue';
export default defineComponent({
setup(){
var songs = [{id: "song1"},{id: "song2"}]
const pauseOthers = () => {
//this is the part that I need to figure out
}
return { songs, pauseOthers }
}
})
</script>
The components's script looks like
<script>
import { defineComponent, ref } from 'vue';
export default defineComponent({
setup(){
const pause = () => {
console.log("Paused!");
}
return { pause }
}
})
</script>
Ideally this could avoid using third-party packages, but it's fine if there really is no easy way.
Basically what you are going to want to do is create a a ref on the component which contains the v-for loop (i.e. the parent component) that is something like const currentlyPlayingPlayerIndex = ref(-1) then provide that as a prop to your AudioPlayer component. In AudioPlayer you use a watcher that watches for the value of that prop to change, if the index of that player (order in the v-for list) matches the value of currentlyPlayingPlayerIndex then take whatever action is required to play the audio. If it doesn't match, take whatever action is required to pause the audio. This will ensure that only one player is ever playing at once.
Within the player component the "pause" button should emit an event that sets the value of currentlyPlayingPlayerIndex to -1 or null or something that is outside the bounds of the range of the indexes. The "play" button should emit an event to the parent that updates the value of currentlyPlayingPlayerIndex to the index of the player component that contained the clicked button.
In summary, don't manage the "playing" state on the level of player but instead on their parent.
I agree with #WillD, you should manage those events from the parent or you will overcomplicate it.
Example:
<!-- Parent -->
<template>
<AudioPlayer v-for="song in songs" :key="song.id" :song-id="song.id" #play="pauseOthers" />
</template>
<script lang="ts" setup>
const songs = [
{ id: "song1" },
{ id: "song2" },
{ id: "song3" },
{ id: "song4" },
{ id: "song5" }
];
const pauseOthers = (songId: string) => {
const songsToStop = songs.filter(song => song.id !== songId);
console.log("songs to stop:", songsToStop.map(song => song.id).join(" "));
};
</script>
<!-- Audioplayer children component -->
<template>
<button block #click="emit('play', props.songId)">
something
</button>
</template>
<script lang="ts" setup>
const props = defineProps({
songId: {
type: String,
required: true
}
});
const emit = defineEmits(["play"]);
</script>
Note that all those props and emits are just created for being able to reproduce an example, you probably won't need them in your final code.
I am wondering how to destructure an object prop without having to type data.title, data.keywords, data.image etc. I've tried spreading the object directly, but inside the template it is undefined if I do that.
Would like to return directly {{ title }}, {{ textarea }} etc.
My code:
<template>
<div>
<h1>{{ title }}</h1>
</div>
</template>
<script lang="ts">
import { useSanityFetcher } from "vue-sanity";
import { defineComponent, reactive, toRefs } from "vue";
export default defineComponent({
name: "App",
setup: () => {
const articleQuery = `*[_type == "article"][0] {
title,
textarea,
}`;
const options = {
listen: true,
clientOnly: true,
};
const res = useSanityFetcher<any | object>(articleQuery, options);
const data = reactive(res.data);
return toRefs(data);
},
});
</script>
Considering that useSanityFetcher is asynchronous, and res is reactive, it's incorrect to access res.data directly in setup because this disables the reactivity. Everything should happen in computed, watch, etc callback functions.
title, etc properties need to be explicitly listed in order to map reactive object to separate refs with respective names - can probably be combined with articleQuery definition or instantly available as res.data keys
E.g.:
const dataRefs = Object.fromEntries(['title', ...].map(key => [key, ref(null)]))
const res = ...
watchEffect(() => {
if (!res.data) return;
for (const key in dataRefs)
dataRefs[key] = res.data[key];
});
return { ...dataRefs };
Destructuring the object is not the problem, see Vue SFC Playground
<script lang="ts">
//import { useSanityFetcher } from "vue-sanity";
import { defineComponent, reactive, toRefs } from "vue";
export default defineComponent({
name: "App",
setup: () => {
const res = {
data: {
title: 'Hi there'
}
}
const data = reactive(res.data);
return toRefs(data);
},
});
</script>
<template>
<div>
<h1>{{ title }}</h1>
</div>
</template>
It may simply be the space between the filter and the projection in the GROQ expression
const articleQuery = `*[_type == "article"][0]{ title, textarea }`;
See A description of the GROQ syntax
A typical GROQ query has this form:
*[ <filter> ]{ <projection> }
The Vue docs actually recommend not destructing props because of the way reactivity works but if you really want to something like this should work:
const res = useSanityFetcher<any | object(articleQuery, options);
const data = reactive(res.data);
return toRefs(data);
Don't forget to import reactive and toRefs.
I have a loop, in which I get several result from my API call.
Each of them has an property called processing_time that shows how many seconds it has left until something happens.
How can I implement the countdown for each interval?
Here is my <progressbar> component which needs a value for initially displaying the progress (I need it to be dynamic)
<progress-bar :options="options"
:value="data.properties.processing_time"
/>
<span class="progress-bar-seconds"
>{{ data.properties.processing_time }} Seconds left
...</span>
I feel like I need to use computed but I don't exactly know how to do it.
A computed prop is probably not the best solution, since you need to change the timer value locally. I recommend using a local copy that is updated periodically:
Create a local data property (a ref).
Copy the value prop to the local data prop upon mounting.
Use setInterval to periodically decrement the value.
<template>
<div>
<span class="progress-bar-seconds">{{ seconds }} Seconds left ...</span>
</div>
</template>
<script>
import { ref, onMounted, onUnmounted } from 'vue'
export default {
props: {
value: {
type: Number,
default: 0,
required: true,
},
},
setup(props) {
const seconds = ref(0) // 1️⃣
let _timerId = null
onMounted(() => {
seconds.value = props.value // 2️⃣
if (seconds.value > 0) {
// 3️⃣
_timerId = setInterval(() => {
seconds.value--
if (seconds.value <= 0) {
clearInterval(_timerId)
}
}, 1000)
}
})
onUnmounted(() => clearInterval(_timerId))
return { seconds }
}
}
</script>
demo
<template>
<div>
<h1>{{ counter }}</h1>
<input type="text" v-model="counter" />
</div>
</template>
<script>
import { computed } from 'vue'
import { useStore } from 'vuex'
export default {
setup() {
const store = useStore()
const counter = computed(() => store.state.counter)
return { counter }
},
}
</script>
How to change value of counter in the store when input value changes
I am getting this error in the console:
[ operation failed: computed value is readonly ]
Try this:
...
const counter = computed({
get: () => store.state.counter,
set: (val) => store.commit('COUNTER_MUTATION', val)
})
...
https://v3.vuejs.org/api/computed-watch-api.html#computed
Try this:
<input v-model="counter">
computed: {
counter: {
get () {
return this.$store.state.a
},
set (value) {
this.$store.commit('updateA', value)
}
}
}
With composition API
When creating computed properties we can have two types, the readonly and a writable one.
To allow v-model to update a variable value we need a writable computed ref.
Example of a readonly computed ref:
const
n = ref(0),
count = computed(() => n.value);
console.log(count.value) // 0
count.value = 2 // error
Example of a writable computed ref:
const n = ref(0)
const count = computed({
get: () => n.value,
set: (val) => n.value = val
})
count.value = 2
console.log(count.value) // 2
So.. in summary, to use v-model with Vuex we need to use a writable computed ref. With composition API it would look like below:
note: I changed the counter variable with about so that the code makes more sense
<script setup>
import {computed} from 'vue'
import {useStore} from 'vuex'
const store = useStore()
const about = computed({
get: () => store.state.about,
set: (text) => store.dispatch('setAbout', text)
})
</script>
<template>
<div>
<h1>{{ about }}</h1>
<input type="text" v-model="about" />
</div>
</template>