Vue 3: v-model and emit - javascript

I'm trying to use Vue3 two-way biding with v-model, but my emit() doesn't update the parent value. Could you please tell me where I'm wrong?
Thank you!
Parent looks like:
<template>
<div class="card">
<LearnersTable
v-model:toActivate="toActivate"
/>
<!-- To control if this is being updated -->
{{ toActivate.length }}
</div>
</template>
<script setup>
[...]
// List of person to activate
const toActivate = [];
</script>
And Children (LearnersTable) looks like:
<template>
[...]
<tr v-for="row in rows" :key="row.id" >
<span>
<Toggle v-model="row.enabled" #change="onChangeActivate(row)"/>
</span>
</tr>
[...]
</template>
<script setup>
const props = defineProps({
toActivate: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(['update:toActivate']);
const {
toActivate,
} = toRefs(props);
function onChangeActivate(row) {
if (row.enabled === true) {
toActivate.value.push(row);
}
emit('update:toActivate', toActivate.value);
}
</script>
I'm omitting a little bit of code here. But the problem is that my emit doesn't work, I don't get the toActivate value updated in the parent.
Thank you !

Try to make it reactive:
<script setup>
import { ref } from 'vue';
// List of person to activate
const toActivate = ref([]);
</script>
and
<LearnersTable
v-model:to-activate="toActivate"
/>

Related

Please note that slots are not reactive

Why is slot said to be no reactive?
https://v2.vuejs.org/v2/api/index.html#vm-slots
Following is the example, when i click component's button, the headerValue will add, if the slot is no reactive, then the component childern will not render, but actually it's opposite
<!-- component A -->
<template>
<div>
<header-slot>
<template slot="header">
<div>{{ headerValue }}</div>
</template>
</header-slot>
<button #click="changeProp">change</button>
</div>
</template>
<script>
import HeaderSlot from '#/views/headerSlot.vue'
export default {
components: {
HeaderSlot
},
data() {
return {
headerValue: 1
}
},
methods: {
changeProp() {
this.headerValue += 1
}
}
}
</script>
<template>
<!-- component children -->
<h1>
inner-text
<slot name="header" />
</h1>
</template>
The example you're referring to is using this snippet of code
Vue.component('blog-post', {
render: function (createElement) {
var header = this.$slots.header // 👈🏻 referring to that slot
var body = this.$slots.default
var footer = this.$slots.footer
return createElement('div', [
createElement('header', header),
createElement('main', body),
createElement('footer', footer)
])
}
})
aka using this.$slots.header in a render function is probably not reactive.
Of course, if you have a slot in place and you have some updates on a data or computed state, it will be reactive and update itself but here, the given point is about the vm.$slots (which is read only btw).

Vue.js 3 - watching props in child component does not work if props comes with a member of array in parent component

I am working with Vue.js 3. I got a problem, let us see the code first.
Code
ChildComponent.vue
<template>
<div>
{{ modelValue }}
<input v-model="resultString"/>
<button #click="showModelValue">show model value</button>
</div>
</template>
<script lang="ts">
import {defineComponent, PropType, ref, watch} from "vue";
export default defineComponent({
props: {
modelValue: {
type: Object as PropType<number>,
required: true,
}
},
emits:['update:modelValue'],
setup(props) {
const resultString = ref<string>("");
watch(() => props.modelValue, (newVal:number, oldVal:number) => {
if (newVal % 2 == 0) {
resultString.value = 'even';
} else {
resultString.value = 'odd';
}
}, {deep: true});
const showModelValue = () => {
console.log(props.modelValue);
}
return { resultString, showModelValue }
}
})
</script>
<style scoped>
</style>
ParentComponent.vue
<template>
<div class="main-container">
<child-component v-model="test1" />
<button #click="increaseTest1">increase test1</button>
<hr/>
Cannot use v-model within v-for!
<!--
<div v-for="(testNum, index) in test2">
<child-component v-model="testNum" /> <button #click="increaseTest2(index)">increase test2</button>
</div>
-->
<hr/>
<div v-for="(testNumWrapper, index) in test3">
<child-component v-model="testNumWrapper.val" /> <button #click="increaseTest3(index)">increase test3</button>
</div>
</div>
</template>
<script lang="ts">
import {defineComponent, onMounted, ref} from "vue";
import ChildComponent from "#/main/components/pc/ChildComponent.vue";
export default defineComponent({
components: {ChildComponent},
setup() {
const test1 = ref<number>(1);
const increaseTest1 = () => test1.value++;
/*
const test2 = ref<number[]>([3,1,4,1,5,9]);
const increaseTest2 = (index:number) => test2.value[index]++;
const updateTest2 = (e:any) => {
console.log(e);
};
*/
const test3 = ref<{val:number}[]>([]);
const increaseTest3 = (index:number) => test3.value[index].val++;
onMounted(() => {
// This triggers watch() in childComponent.
test1.value = 4;
// But these do NOT trigger watch() in childComponent.
test3.value = [{val: 3},{val: 1},{val: 4},{val: 1},{val: 5},{val: 9}];
});
return {
test1, increaseTest1,
//test2, increaseTest2, updateTest2,
test3, increaseTest3,
}
}
});
</script>
<style scoped>
</style>
The above codes are modified for sharing my problem, let me explain.
The ChildComponent decides if the value of modelValue is odd or even, automatically.
The ParentComponent...
binds a ref variable, test1 to ChildComponent,
tries to bind each primitive typed member of ref array variable, test2 to ChildComponents but this is not compiled because v-model cannot be used within v-for, so that I commented out the code lines and try the next test,
binds each non-primitive typed member of ref array variable, test3 to ChildComponent.
And it initiates the variables in onMounted().
However, I've found that watch() in ChildCompoent works fine for test1 but not for test3. The watch() also does not work for test3 when I push a value into test3 or delete a value from test3. (It works when I click increase button.)
Please, show me a way to trigger the watch() function for test3.
Thank you.
Of course the watch in child component is not triggered by pushing or deleting elements from the array. Child component is not watching whole array but just single element (it's val property).
If you push new element into the array, child component for that element does not exist yet. If you delete an element, the child component rendered for that element is destroyed immediately. Only thing that can trigger that watch is indeed mutation of the val property...
Problem with test2 is that testNum is local temporary variable - instead of v-model="testNum", use v-model="test2[index]"
Anyway your ChildComponent.vue does not need watch at all. Just use computed:
const resultString = computed(() => props.modelValue % 2 === 0 ? 'even' : 'odd')
...and btw you should be using key with v-for - docs

this.$emit() is not a function while trying to change state in parent component from child in composition API in vue 3

//Parent component
<template>
<childComp #onchangeData='changeData' />
</template>
<script>
setup() {
const state = reactive({
data: 'anything
});
function changeData(v){
state.data = v
}
return { changeData}
},
</script>
//Child
<template>
<button #click='change('hello')' />
</template>
<script>
setup() {
function change(v){
this.$emit('onchangeData', v)
}
return{change}
},
</script>
I am struggling to change the parents' reactive state from the child's button click. It's saying this.$emit is not a function. I tried many ways like using #onchangeData='changeData()' instead of #onchangeData='changeData', using arrow functions etc. But nothing works. Here, I wrote an example and minimal code to keep it simple. But I hope my problem is clear.
Look at following snippet, this is not the same in composition as in options API, so you need to use emit passed to setup function:
const { reactive } = Vue
const app = Vue.createApp({
setup() {
const state = reactive({
data: 'anything'
});
function changeData(v){
state.data = v
}
return { changeData, state }
},
})
app.component("ChildComp", {
template: `
<div>
<button #click="change('hello')">click</button>
</div>
`,
setup(props, {emit}) {
function change(v){
emit('onchangeData', v)
}
return { change }
},
})
app.mount('#demo')
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
<div id="demo">
<child-comp #onchange-data='changeData'></child-comp>
<p>{{ state.data }}</p>
</div>

vuejs how to access a ref inside a child component?

I am new to vuejs and not very experienced in JavaScript. I use Vue3 in Laravel
I have a child component which exposes a ref on an input like this
<input
v-model="raw_input"
ref="raw"
#input="checkLength(limit)"
#keydown="enterNumbers($event)"
type="number"
/>
const raw = ref('');
defineExpose({
raw
})
In the parent
<template lang="">
<PageComponent title="Dashboard">
<SmartVolumeInputVue ref="svi" />
<div class="mt-16 border h-8">
{{ val }}
</div>
</PageComponent>
</template>
<script setup>
import PageComponent from "../components/PageComponent.vue"
import SmartVolumeInputVue from "../components/customInputs/SmartVolumeInput.vue";import { computed, ref } from "vue";
const svi = ref(null)
//let val=svi
//let val=svi.raw
let val=svi.raw.value
</script>
At the bottom of the script there are 3 lines (I only uncomment one at a time)
The first displays this in the template
{ "raw": "[object HTMLInputElement]" }
The second displays nothing
And the third reports an error
Uncaught (in promise) TypeError: svi.raw is undefined
I need some help to get the value of the referenced input in the child component.
Do you need the ref on the element? Or only the value?
If not, just expose the value like this: Live example
// App.vue
<script setup>
import { ref } from 'vue'
import SmartVolumeInputVue from './SmartVolumeInput.vue'
const svi = ref()
</script>
<template>
<SmartVolumeInputVue ref="svi" />
<div>
{{svi?.raw_input}}
</div>
</template>
// SmartVolumeInput.vue
<script setup>
import { ref, defineExpose } from 'vue'
const raw_input = ref(123)
defineExpose({
raw_input
})
</script>
<template>
<input v-model.number="raw_input" type="number" />
</template>

How to get $refs using Composition API in Vue3?

I am trying to get $refs in Vue 3 using Composition API. This is my template that has two child components and I need to get reference to one child component instance:
<template>
<comp-foo />
<comp-bar ref="table"/>
</template>
In my code I use Template Refs: ref is a special attribute, that allows us to obtain a direct reference to a specific DOM element or child component instance after it's mounted.
If I use Options API then I don't have any problems:
mounted() {
console.log("Mounted - ok");
console.log(this.$refs.table.temp());
}
However, using Composition API I get error:
setup() {
const that: any = getCurrentInstance();
onMounted(() => {
console.log("Mounted - ok");
console.log(that.$refs.table.temp());//ERROR that.$refs is undefined
});
return {};
}
Could anyone say how to do it using Composition API?
You need to create the ref const inside the setup then return it so it can be used in the html.
<template>
<div ref="table"/>
</template>
import { ref, onMounted } from 'vue';
setup() {
const table = ref(null);
onMounted(() => {
console.log(table.value);
});
return { table };
}
On Laravel Inertia:
<script setup>
import { ref, onMounted } from "vue";
// a list for testing
let items = [
{ id: 1, name: "item name 1" },
{ id: 2, name: "item name 2" },
{ id: 3, name: "item name 3" },
];
// this also works with a list of elements
let elements = ref(null);
// testing
onMounted(() => {
let all = elements.value;
let item1 = all[0];
let item2 = all[1];
let item3 = all[2];
console.log([all, item1, item2, item3]);
});
</script>
<template>
<div>
<!-- elements -->
<div v-for="(item, i) in items" ref="elements" :key="item.id">
<!-- element's content -->
<div>ID: {{ item.id }}</div>
<div>Name: {{ item.name }}</div>
</div>
</div>
</template>
<template>
<your-table ref="table"/>
...
</template>
<script>
import { ref, onMounted } from 'vue';
setup() {
const table = ref(null);
onMounted(() => {
table.value.addEventListener('click', () => console.log("Event happened"))
});
return { table };
}
</script>
Inside your other component you can interact with events you already registered on onMounted life cycle hook as with my example i've registered only one evnet
If you want, you can use getCurrentInstance() in the parent component like this code:
<template>
<MyCompo ref="table"></MyCompo>
</template>
<script>
import MyCompo from "#/components/MyCompo.vue"
import { ref, onMounted, getCurrentInstance } from 'vue'
export default {
components : {
MyCompo
},
setup(props, ctx) {
onMounted(() => {
getCurrentInstance().ctx.$refs.table.tempMethod()
});
}
}
</script>
And this is the code of child component (here I called it MyCompo):
<template>
<h1>this is MyCompo component</h1>
</template>
<script>
export default {
setup(props, ctx) {
const tempMethod = () => {
console.log("temporary method");
}
return {
tempMethod
}
},
}
</script>

Categories