$forceUpdate (or similar) in vue3 - How implement it? - vue.js

I have a Vue component. This component has a computed prop & a watch:
const resetComponent = computed(()=>{
return store.state.filtros.room_amount
})
watch(resetComponent, () => {
if(resetComponent.value.compare == '>' && resetComponent.value.valor == '' ){
console.log('RESET COMPONENT')
}
})
My console.log('RESET COMPONENT') runs correctly, when it should.
But instead, i want re-render all my component, that is, return to its initial state. There's some way?
This is my full component
<template>
<FiltroCantidad :data="data" />
</template>
<script>
import SelectButton from "primevue/selectbutton";
import FiltroCantidad from "../utils/FiltroCantidad.vue";
import { computed, ref, watch } from "vue";
import { useStore } from 'vuex';
export default {
setup(props, context) {
const store = useStore()
const data = ref({
label: "Ambientes",
value: "room_amount",
action: "roomAmountAction",
});
const resetComponent = computed(()=>{
return store.state.filtros.room_amount
})
watch(resetComponent, () => {
if(resetComponent.value.compare == '>' && resetComponent.value.valor == '' ){
console.log('RESET COMPONENT')
}
})
return { data, resetComponent };
},
components: {
SelectButton,
FiltroCantidad,
},
};
</script>

One way to re-render the component is to apply a key attribute that changes:
<FiltroCantidad :data="data" :key="myKey" />
export default {
setup() {
//...
const myKey = ref(0)
watch(resetComponent, () => {
if(/* need to reset */) {
myKey.value++
}
})
return { myKey }
}
}

Related

Vue 3 - Composition API fetching data?

I am a bit confused with composition API and fetching data. When I open the page, I can see rendered list of categories, but if I want to use categories in setup(), it is undefined. How can I use categories value inside setup function? You can see that I want to console log categories.
Category.vue
<template>
<div class="page-container">
<item
v-for="(category, index) in categories"
:key="index"
:item="category"
:is-selected="selectedItem === index"
#click="selectItem(index)"
/>
</div>
</template>
<script>
import { computed, ref } from 'vue'
import { useStore } from 'vuex'
import Item from '#/components/Item.vue'
export default {
components: {
Item
},
setup () {
const store = useStore()
store.dispatch('categories/getCategories')
const categories = computed(() => store.getters['categories/getCategories'])
const selectedItem = ref(1)
const selectItem = (index) => {
selectedItem.value = index
}
console.log(categories.value[selectedItem.value].id)
return {
categories,
selectedItem,
selectItem
}
}
}
</script>
<style lang="scss" scoped>
#import '#/assets/scss/general.scss';
</style>
categories.js - vuex module
import axios from 'axios'
import { API_URL } from '#/helpers/helpers'
export const categories = {
namespaced: true,
state: {
categories: []
},
getters: {
getCategories: (state) => state.categories
},
mutations: {
UPDATE_CATEGORIES: (state, newValue) => { state.categories = newValue }
},
actions: {
async getCategories ({ commit }) {
await axios.get(`${API_URL}/getCategories.php`).then(response => {
commit('UPDATE_CATEGORIES', response.data.res_data.categories)
})
}
},
modules: {
}
}
In the setup function you cannot process a computed function.
You can instead access store.getters['categories/getCategories'].value[selectedItem.value].id if you want to process that in the setup function.

How to use Pinia with Nuxt, composition-api (vue2) and SSR?

I'm trying to get Pinia to work in Nuxt with SSR (server-side rendering).
When creating a page without Pinia, it works:
<script>
import { reactive, useFetch, useContext } from '#nuxtjs/composition-api'
export default {
setup() {
const { $axios } = useContext()
const invitesStore = reactive({
invites: [],
loading: true,
})
useFetch(async () => {
invitesStore.loading = true
await $axios.$get('invite/registermember').then((result) => {
invitesStore.loading = false
invitesStore.invites = result.invites
})
})
return {
invitesStore,
}
},
}
</script>
But when introducing Pinia, I get the error "Converting circular structure to JSON --> starting at object with constructor 'VueRouter'"
I'm using Pinia this way:
// /store/invitesStore.js
import { defineStore } from 'pinia'
// useStore could be anything like useUser, useCart
export const useInvitesStore = defineStore({
// unique id of the store across your application
id: 'storeId',
state() {
return {
invites: [],
loading: true,
}
},
})
<script>
import { useInvitesStore } from '#/store/invitesStore'
import { reactive, onMounted, useFetch, useContext } from '#nuxtjs/composition-api'
export default {
setup() {
const { $axios } = useContext()
const invitesStore = useInvitesStore()
useFetch(async () => {
invitesStore.loading = true
await $axios.$get('invite/registermember').then((result) => {
invitesStore.loading = false
invitesStore.invites = result.invites
})
})
return {
invitesStore,
}
},
}
</script>
Is it possible to get this to work? How?

Vue 3 access child component from slots

I am currently working on a custom validation and would like to, if possible, access a child components and call a method in there.
Form wrapper
<template>
<form #submit.prevent="handleSubmit">
<slot></slot>
</form>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
setup(props, { slots }) {
const validate = (): boolean => {
if (slots.default) {
slots.default().forEach((vNode) => {
if (vNode.props && vNode.props.rules) {
if (vNode.component) {
vNode.component.emit('validate');
}
}
});
}
return false;
};
const handleSubmit = (ev: any): void => {
validate();
};
return {
handleSubmit,
};
},
});
</script>
When I call slot.default() I get proper list of child components and can see their props. However, vNode.component is always null
My code is based from this example but it is for vue 2.
If someone can help me that would be great, or is this even possible to do.
I found another solution, inspired by quasar framework.
Form component provide() bind and unbind function.
bind() push validate function to an array and store in Form component.
Input component inject the bind and unbind function from parent Form component.
run bind() with self validate() function and uid
Form listen submit event from submit button.
run through all those validate() array, if no problem then emit('submit')
Form Component
import {
defineComponent,
onBeforeUnmount,
onMounted,
reactive,
toRefs,
provide
} from "vue";
export default defineComponent({
name: "Form",
emits: ["submit"],
setup(props, { emit }) {
const state = reactive({
validateComponents: []
});
provide("form", {
bind,
unbind
});
onMounted(() => {
state.form.addEventListener("submit", onSubmit);
});
onBeforeUnmount(() => {
state.form.removeEventListener("submit", onSubmit);
});
function bind(component) {
state.validateComponents.push(component);
}
function unbind(uid) {
const index = state.validateComponents.findIndex(c => c.uid === uid);
if (index > -1) {
state.validateComponents.splice(index, 1);
}
}
function validate() {
let valid = true;
for (const component of state.validateComponents) {
const result = component.validate();
if (!result) {
valid = false;
}
}
return valid;
}
function onSubmit() {
const valid = validate();
if (valid) {
emit("submit");
}
}
}
});
Input Component
import { defineComponent } from "vue";
export default defineComponent({
name: "Input",
props: {
rules: {
default: () => [],
type: Array
},
modelValue: {
default: null,
type: String
}
}
setup(props) {
const form = inject("form");
const uid = getCurrentInstance().uid;
onMounted(() => {
form.bind({ validate, uid });
});
onBeforeUnmount(() => {
form.unbind(uid);
});
function validate() {
// validate logic here
let result = true;
props.rules.forEach(rule => {
const value = rule(props.modelValue);
if(!value) result = value;
})
return result;
}
}
});
Usage
<template>
<form #submit="onSubmit">
<!-- rules function -->
<input :rules="[(v) => true]">
<button label="submit form" type="submit">
</form>
</template>
In the link you provided, Linus mentions using $on and $off to do this. These have been removed in Vue 3, but you could use the recommended mitt library.
One way would be to dispatch a submit event to the child components and have them emit a validate event when they receive a submit. But maybe you don't have access to add this to the child components?
JSFiddle Example
<div id="app">
<form-component>
<one></one>
<two></two>
<three></three>
</form-component>
</div>
const emitter = mitt();
const ChildComponent = {
setup(props, { emit }) {
emitter.on('submit', () => {
console.log('Child submit event handler!');
if (props && props.rules) {
emit('validate');
}
});
},
};
function makeChild(name) {
return {
...ChildComponent,
template: `<input value="${name}" />`,
};
}
const formComponent = {
template: `
<form #submit.prevent="handleSubmit">
<slot></slot>
<button type="submit">Submit</button>
</form>
`,
setup() {
const handleSubmit = () => emitter.emit('submit');
return { handleSubmit };
},
};
const app = Vue.createApp({
components: {
formComponent,
one: makeChild('one'),
two: makeChild('two'),
three: makeChild('three'),
}
});
app.mount('#app');

Migrating "detect click outside" custom directive from Vue 2 to Vue 3

Based on this question Detect click outside element and this answer https://stackoverflow.com/a/42389266, I'm trying to migrate the directive from Vue 2 to Vue 3. It seems that binding.expression and vnode.context not exists more. How can I make it work?
app.directive('click-outside', {
beforeMount (el, binding, vnode) {
el.clickOutsideEvent = function (event) {
if (!(el === event.target || el.contains(event.target))) {
vnode.context[binding.expression](event);
}
};
document.body.addEventListener('click', el.clickOutsideEvent);
},
unmounted (el) {
document.body.removeEventListener('click', el.clickOutsideEvent);
}
});
You can use binding.value instead like this:
const { createApp } = Vue;
const highlightEl = (color ) => (event, el) => {
if (el) {
el.style.background = color;
} else {
event.target.style.background = color;
}
}
const clearHighlightEl = (event, el) => {
if (el) {
el.style.background = '';
} else {
event.target.style.background = '';
}
}
const app = Vue.createApp({
setup() {
return {
highlightEl,
clearHighlightEl
}
}
})
app.directive('click-outside', {
mounted(el, binding, vnode) {
el.clickOutsideEvent = function(event) {
if (!(el === event.target || el.contains(event.target))) {
binding.value(event, el);
}
};
document.body.addEventListener('click', el.clickOutsideEvent);
},
unmounted(el) {
document.body.removeEventListener('click', el.clickOutsideEvent);
}
});
app.mount('#app')
<script src="https://unpkg.com/vue#3.0.0-rc.11/dist/vue.global.prod.js"></script>
<div id="app">
<h1 v-click-outside="highlightEl('yellow')" #click="clearHighlightEl">Element 1</h1>
<p v-click-outside="highlightEl('#FFCC77')" #click="clearHighlightEl">Element 2</p>
</div>
out of the context, there's an easier way in vue3 with composition.
Link to Vueuse ClickOutside (Vue 3)
Link to Vueuse ClickOutside(Vue 2)
<template>
<div ref="target">
Hello world
</div>
<div>
Outside element
</div>
</template>
<script>
import { ref } from 'vue'
import { onClickOutside } from '#vueuse/core'
export default {
setup() {
const target = ref(null)
onClickOutside(target, (event) => console.log(event))
return { target }
}
}
</script>
you can use ref to find out if the element contains the element clicked
<template>
<div ref="myref">
Hello world
</div>
<div>
Outside element
</div>
</template>
<script>
export default {
data() {
return {
show=false
}
},
mounted(){
let self = this;
document.addEventListener('click', (e)=> {
if (self.$refs.myref !==undefined && self.$refs.myref.contains(e.target)===false) {
//click outside!
self.show = false;
}
})
}
}
</script>
vue2 solution:
<script>
export default {
name: 'onClickOutside',
props: ['clickOutside'],
mounted() {
const listener = e => {
if (e.target === this.$el || this.$el.contains(e.target)) {
return
}
this.clickOutside()
}
document.addEventListener('click', listener)
this.$once('hook:beforeDestroy', () => document.removeEventListener('click', listener))
},
render() {
return this.$slots.default[0]
},
}
</script>
vue3:
<script>
import { getCurrentInstance, onMounted, onBeforeUnmount, ref, defineComponent } from 'vue'
export default defineComponent({
name: 'OnClickOutside',
props: ['clickOutside'],
setup(props, { emit, attrs, slots }) {
const vm = getCurrentInstance()
const listener = event => {
const isClickInside = vm.subTree.children.some(element => {
const el = element.el
return event.target === el || el.contains(event.target)
})
if (isClickInside) {
console.log('clickInside')
return
}
props.clickOutside && props.clickOutside()
}
onMounted(() => {
document.addEventListener('click', listener)
})
onBeforeUnmount(() => {
document.removeEventListener('click', listener)
})
return () => slots.default()
},
})
</script>

How to add the total + 1 in the text span each time a notification using vue.js 2?

My vue component is like this :
<template>
...
<span v-if="total > 0" class="badge" id="total">{{ total }}</span>
...
</template>
<script>
import { mapGetters } from 'vuex'
export default {
mounted() {
this.initialMount()
},
computed: {
...mapGetters(['total'])
},
methods: {
initialMount() {
Echo.private('App.User.' + window.Laravel.authUser.id).notification((notification) => {
const a = $('#total').text()
const b= parseInt(a) + 1
$('#total').text(b)
})
},
}
}
</script>
My modules is like this :
import { set } from 'vue'
import notification from '../../api/notification'
import * as types from '../mutation-types'
const state = {
total: 0,
}
const getters = {
total: state => state.total
}
const actions = {
getNotificationList ({ commit,state })
{
notification.getList(
data => {
const notifications = data
commit(types.GET_NOTIFICATION,{ notifications });
},
errors => {
console.log(errors)
}
)
}
}
const mutations = {
[types.GET_NOTIFICATION] (state, { notifications }) {
state.total = notifications.length
}
}
export default {
state,
getters,
actions,
mutations
}
===================================================================
I want every notification, the notification number incremented by 1
My above code works, but it still using jquery
I want change it using vue.js
How can I do it?
You have to commit action into the sucess callback of Echo, but first you have to define mutation:
const mutations = {
[types.GET_NOTIFICATION] (state, { notifications }) {
state.total = notifications.length
},
inc (state) {
state.total++
}
}
And then, you can commit action
methods: {
initialMount() {
Echo.private('App.User.' + window.Laravel.authUser.id).notification((notification) => {
// Make sure you have imported store
store.commit('inc')
})
},
}