vue 3 composition API, conditional rendering - vue.js

I have a button that needs to change it's icon when clicked in order to toggle between play and pause.
Here is a minimalistic sample of the code :
<template>
<div #click="toggleF">
<i v-if="toggleForce == true" class="fas fa-pause"></i>
<i v-if="toggleForce == false" class="fas fa-play"></i>
</div>
</template>
<script>
import {onMounted, onBeforeMount, ref} from 'vue'
export default {
setup(){
const toggleForce = ref(false)
function toggleF () {
toggleForce.value = !toggleForce.value
};
return {toggleF,toggleForce}
}
}
</script>
I get the following warning/error :

please try the code below:
demo 1
<template>
<div #click="toggleForce = !toggleForce">
<i v-if="toggleForce" class="fas fa-pause" />
<i v-else class="fas fa-play" />
</div>
</template>
<script setup>
import {ref} from 'vue'
const toggleForce = ref(false)
</script>
the <script setup> tag is available since the vue version 3.2 and will help you to clean up your script area
demo 2
const { ref, createApp } = Vue
createApp({
setup() {
const toggleForce = ref(false)
const toggleF = () => {
toggleForce.value = !toggleForce.value
}
return {
toggleForce,
toggleF
}
}
}).mount('#app')
<script src="https://unpkg.com/vue#next"></script>
<div id="app">
<button #click="toggleF">toggleForce
<div v-if="toggleForce == true" class="fas fa-pause">TRUE</div>
<div v-if="toggleForce == false" class="fas fa-play">FALSE</div>
</button>
</div>

Related

How do I use the mounted function in VueJs 3 composition api?

I am trying out the composition api in vueJs 3. Unfortunately I don't know how to include my mounted code in the setup(). When I put the code into the setup, the javascript tries to access the not yet rendered DOM. Can anyone tell me how I have to rewrite this?
Option api sytle (works)
<script>
export default {
name: "NavBar",
data() {
return {};
},
methods: {},
mounted() {
const bm = document.querySelector('#toggle');
const menu = document.querySelectorAll('nav ul li');
const overlay = document.querySelector('#overlay');
// ...
bm.addEventListener('click', () => {
bm.classList.toggle("active");
overlay.classList.toggle("open");
})
},
}
</script>
<template>
<header>
<div class="button_container" id="toggle">
<span class="top"></span>
<span class="middle"></span>
<span class="bottom"></span>
</div>
<div class="overlay" id="overlay">
<nav class="overlay-menu">
<ul>
<li>Home</li>
<!-- ... -->
</ul>
</nav>
</div>
</header>
</template>
First of all, this not a good practice to get a html element via document.querySelector(), see how to bind events in vue3
// bad
<div class="button_container" id="toggle">...</div>
mounted() {
const bm = document.querySelector('#toggle');
bm.addEventListener('click', () => {
bm.classList.toggle("active");
overlay.classList.toggle("open");
})
}
// good
<div class="button_container" id="toggle" #click="toggleContainer">...</div>
methods: {
toggleContainer() {
...
}
}
If you really want to document.querySelector() in setup(), you can use onMounted
import { onMounted } from 'vue';
export default {
setup() {
onMounted(() => {
const bm = document.querySelector('#toggle');
bm.addEventListener('click', () => {
bm.classList.toggle("active");
overlay.classList.toggle("open");
})
});
}
}
// or inside <script setup>
import { onMounted } from 'vue';
onMounted(() => {
....
});
But, just as #kissu commented, ref is a better way if you have to handle the html tag directly in vue
<div
ref="toggler">
...
</div>
<script setup>
import { ref, onMounted } from 'vue';
const toggler = ref(null);
onMounted(() => {
console.log(toggler.value) // <div></div>
});
</script>
But none of the above follow the concept of Vue, which is Vue is driven by data.
Since you seem to create a click event listener which will effect the class in html, here is the Vue way:
<template>
<header>
<div
class="button_container"
id="toggle"
:class="{
'active': isActiveBm
}"
#click="toggle">
<span class="top"></span>
<span class="middle"></span>
<span class="bottom"></span>
</div>
<div
class="overlay"
id="overlay"
:class="{
'open': isOpenOverlay
}">
<nav class="overlay-menu">
<ul>
<li>Home</li>
<!-- ... -->
</ul>
</nav>
</div>
</header>
</template>
<script setup>
import { ref } from 'vue';
const isActiveBm = ref(false);
const isOpenOverlay = ref(false);
const toggle = () => {
isActiveBm.value = !isActiveBm.value;
isOpenOverlay.value = !isOpenOverlay.value;
}
</script>

Vue 3 - [Vue warn]: Invalid vnode type when creating vnode

I have the following code:
<script setup>
import {ref, defineAsyncComponent, computed, reactive} from 'vue'
import {useMainStore} from '#/store/main.js'
import {useTableStore} from "#/store/tables";
import DefaultLayout from '#/layouts/DefaultLayout.vue'
import { storeToRefs } from 'pinia'
const tableStore = useTableStore()
const { userData } = useMainStore()
tableStore.getTables()
const { freeTables, usedTables } = storeToRefs(useTableStore())
const { tables } = storeToRefs(useTableStore())
const loading = ref(false)
const showOrder = ref(false)
let props = reactive({
orderID: null,
id: null,
})
const tableOrderComponent = computed(() => showOrder.value ? defineAsyncComponent(() =>
import("#/components/TableOrder.vue")) : ''
)
let tableOrder = (table) => {
props.id = parseInt(table.id, 10)
props.orderID = table.activeOrderID || null
showOrder.value = true
}
</script>
<template>
<DefaultLayout>
<template #default>
<div class="row come-in">
<div class="card col">
<div class="card-header">
<span class="text-capitalize"> {{ userData.username }}</span>
</div>
<div class="card-body subheader">
Used Tables ({{ usedTables.length }})
</div>
<ul class="list-group list-group-flush">
<li v-for="table in usedTables">
{{ table.name }}
</li>
</ul>
<div class="card-body subheader">
Free Tables ({{ freeTables.length }})
</div>
<ul class="list-group list-group-flush">
<li v-for="table in freeTables">
{{ table.name }}
<a href="#" v-tooltip="'Add order to the table'" #click="tableOrder(table)">
<font-awesome-icon icon="fas fa-list-alt" />
</a>
</li>
</ul>
</div>
<div class="col">
<table-order-component v-show="showOrder"
:orderID="props.orderID"
:id="props.id"></table-order-component>
</div>
</div>
</template>
</DefaultLayout>
</template>
When the page is loaded I am getting following error (warning):
[vite] connecting... client.ts:16:8
[vite] connected. client.ts:53:14
[Vue warn]: Invalid vnode type when creating vnode: .
at <DefaultLayout>
at <Tables onVnodeUnmounted=fn<onVnodeUnmounted> ref=Ref< undefined > >
at <RouterView>
at <App> runtime-core.esm-bundler.js:38:16
[Vue warn]: Invalid vnode type when creating vnode: .
at <DefaultLayout>
at <Tables onVnodeUnmounted=fn<onVnodeUnmounted> ref=Ref<
Proxy { <target>: Proxy, <handler>: {…} }
> > at <RouterView> at <App> runtime-core.esm-bundler.js:38:16
What am I doing wrong here?
Default layout:
<script setup>
import AppHeader from '#/components/AppHeader.vue'
</script>
<template>
<AppHeader></AppHeader>
<div id="container-fluid">
<main>
<slot></slot>
</main>
</div>
</template>
App Header:
<script setup>
import { useRouter } from 'vue-router'
import {useMainStore} from "#/store/main.js";
const mainStore = useMainStore()
const router = useRouter()
const logout = () => {
mainStore.logout()
.then(() => {
router.push({
name: 'Login'
})
})
}
</script>
<template>
<nav id="nav">
<button #click="logout">Logout</button>
</nav>
</template>
<style>
</style>

Vuejs 3 props are Proxy

I am passing array as a prop to another component, and when I want to read this on mounted in that component, I got Proxy {}. How to read data from this prop? You can see in example when I want to console log prop, result is Proxy {}. I can see all values in HTML structure, but not in the console on mounted.
<template>
<div class="custom-select-container">
<div class="selected-item" #click="openSelect">
<span class="selected-items-text">{{ selectedItem.name }}</span>
<span class="icon-arrow1_b selected-items-icon" :class="{ active: showOptions }" />
</div>
<transition name="fade">
<ul v-show="options.length && showOptions" class="custom-select-options">
<li v-for="(option, index) in options" :key="index" class="custom-select-item">{{ option.name }}</li>
</ul>
</transition>
</div>
</template>
<script>
import { ref, onMounted } from 'vue'
export default {
props: {
options: {
type: Array,
default: () => []
}
},
setup(props) {
let showOptions = ref(false);
let selectedItem = ref(props.options[0])
const openSelect = () => {
showOptions.value = !showOptions.value
}
onMounted(() => {
console.log('test', props.options)
})
return {
openSelect,
showOptions,
selectedItem
}
}
}
</script>
Parent component where I am passing data:
<template>
<div class="page-container">
<div>
<div class="items-title">
<h3>List of categories</h3>
<span>({{ allCategories.length }})</span>
</div>
<div class="items-container">
<div class="item" v-for="(category, index) in allCategories" :key="index">
<span class="item-cell size-xs">{{ index + 1 }}.</span>
<span class="item-cell size-l">{{ category.name }}</span>
</div>
</div>
</div>
<custom-select
:options="allCategories"
/>
</div>
</template>
<script>
import CustomSelect from '../components/Input/CustomSelect'
import { computed } from 'vue'
import { useStore } from 'vuex'
export default {
components: {
CustomSelect
},
computed: {
},
setup() {
const store = useStore()
const allCategories = computed(() => store.getters['categories/getAllCategories'])
return {
allCategories
}
}
}
</script>
That's how reactivity works in Vue3.
use
console.log(JSON.parse(JSON.stringify(data))
or
console.log(JSON.stringify(data, null, 2))
to show the content of proxies in console

Open/Close a modal using Vue3 composition API doesn't work as expected

I want to build a simple open/close Modal, using Vue3 composition API,
but it doesn't work.
If I use v-if (as in the code below) the modal doesn't open, if I use v-show the modal opens but the close button doesn't work.
As an addition, add a eventListener for ESC key, and then remove it on unMounted.
App.vue
<div class="min-h-screen flex items-center justify-center">
<button #click="isModalOpen = true" type="button" class="btn btn-blue">Open Modal</button>
</div>
<announcement-modal
v-if="isModalOpen"
#click="isModalOpen = true"
v-model:isOpen="isModalOpen">
</announcement-modal>
</div>
</template>
<script>
import { ref } from "vue";
import AnnouncementModal from "./components/AnnouncementModal";
export default {
components: {
AnnouncementModal,
},
setup() {
const isModalOpen = ref(false);
return {
isModalOpen,
}
},
}
</script>
Announcement.vue
<div class="text-center">
<button #click="closeModal" type="button" class="btn btn-blue">
Dismiss
</button>
</div>
</div>
</div>
</template>
<script>
import { onUnmounted } from "vue"
export default {
props: ["isOpen"],
setup(props, context) {
onUnmounted(function () {
console.log("after unmounted")
});
function closeModal() {
context.emit("update:is-open", false);
}
return {
closeModal
}
}
}
The #click="isModalOpen = true" is immediately opening it every time you click to close the modal.
Remove this event handler and it should work:
const app = Vue.createApp({
setup() {
const isModalOpen = Vue.ref(false);
return {
isModalOpen
}
},
});
app.component('announcement-modal', {
template: `<div class="text-center">
<button #click="closeModal" type="button" class="btn btn-blue">
Dismiss
</button>
</div>`,
setup(props, context) {
Vue.onUnmounted(function() {
console.log("after unmounted")
});
function closeModal() {
context.emit("update:is-open", false);
}
return {
closeModal
}
}
})
app.mount('#app')
<script src="https://unpkg.com/vue#next"></script>
<div id="app">
<div class="min-h-screen flex items-center justify-center">
<button #click="isModalOpen = true" type="button" class="btn btn-blue">Open Modal</button>
</div>
<announcement-modal v-if="isModalOpen" v-model:is-open="isModalOpen"></announcement-modal>
</div>
Also, as #Dan said, the v-model:isOpen="isModalOpen" shouldn't be camelCase.

Error When clicking 'Print' button using 'Vue HTML to Paper'

I have a vue app and I'm trying to implement the 'Vue HTML to Paper'.
I have followed all the instructions on https://randomcodetips.com/vue-html-to-paper/ but when i click my 'Print' button I get the following error.
Main.js
import Vue from 'vue';
import VueRouter from 'vue-router';
import App from './App.vue';
import { routes } from './routes';
import { store } from './store/store';
import 'bootstrap/dist/css/bootstrap.css';
import 'bootstrap-vue/dist/bootstrap-vue.css';
import VueHtmlToPaper from 'vue-html-to-paper';
const router = new VueRouter({
// Loads page always at the top
scrollBehavior() {
$('.tooltip, .popover').tooltip('hide');
return { x: 0, y: 0 };
},
routes,
});
const options = {
name: '_blank',
specs: [
'fullscreen=yes',
'titlebar=yes',
'scrollbars=yes'
],
styles: [
'https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css',
'https://unpkg.com/kidlat-css/css/kidlat.css'
]
}
Vue.use(VueRouter, VueHtmlToPaper, options);
...
Component
<template>
<div>
<loader v-if="loading" />
<div v-else class="form-group row d-flex justify-content-center">
<div class="col-10">
<div id="printMe" class="card">
<div class="card-header">
<div class="row d-flex align-items-center">
<div class="col">
Order details for: <span class="text-info">{{ 'PACK' + productOrder.id }}</span>
</div>
<div class="col d-flex justify-content-end">
<button class="btn btn-info" #click="print">Print</button>
......
<script>
export default {
data() {
return {
...
output: null
....
methods: {
print () {
// Pass the element id here
this.$htmlToPaper('printMe');
},
...
You can't register multiple plugins in one Vue.use.
Try replacing the line
Vue.use(VueRouter, VueHtmlToPaper, options);
With
Vue.use(VueRouter);
Vue.use(VueHtmlToPaper, options)