Vue.js Toggle vuecomponents - vue.js

I have a very simple Vue application I am building. I am new with using Vue.js. I have 2 vue components and I am trying to toggle between them with a button click. Can I use script code on the main page to toggle between the 2 vue components?
Here is my code. I assume I have to use v-show or v-if on the components or a div/span around the components to show and hide them.
App.vue
<template>
<div id="app">
<img alt="Vue logo" src="./assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js App"/>
<Display v-if="show" />
<button v-on:click="toggleDisplay()">toggle btn</button>
</div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
import Display from './components/Display.vue'
export default {
name: 'App',
components: {
HelloWorld,
Display
},
data: {
return {
selectedComp: "HelloWorld"
}
},
methods: {
toggleDisplay(comp: string) {
this.selectedComp = comp;
}
}
}
</script>
Display.vue
<template>
<div class="post">
<div v-if="loading" class="loading">
Loading...
</div>
<div class="titleBlock text-center">
<div class="row justify-content-center" style="width:100%;">
<div class="col-6">
Service Title for Page
</div>
</div>
</div>
<div v-if="byteArray" class="content">
<img :src="byteArray" />
<div>{{ createImage }}</div>
<p>{{ byteArray }}</p>
</div>
</div>
</template>
<script lang="js">
import Vue from 'vue';
export default Vue.extend({
data() {
return {
loading: false,
post: null,
byteArray: null
};
},
created() {
this.fetchByteArray();
// Hide DisplayVue after 5 seconds
//setTimeout(() => this.byteArray = false, 5000)
//.then(
// setTimeout(() => this.byteArray = true, 2000)
// );
setTimeout(() => this.img = false, 5000)
.then(
setTimeout(() => this.img = true, 2000)
);
},
//watch: {
//}
methods: {
fetchByteArray() {
this.post = true;
this.byteArray = true;
this.loading = null;
fetch('https://localhost:5001/api/Doc/image001.png')
.then(response => response.json())
.then(bytes => {
this.byteArray = "data:image/png;base64," + bytes;
this.loading = false;
return;
})
.catch(console.log("Error"));
}
},
computed: {
createImage() {
return `<img class="img-fluid" src="data:image/png;base64,` + this.byteArray + `" />`;
}
}
});
</script>

Give a try to that one: https://vuejs.org/guide/essentials/component-basics.html#dynamic-components
Mainly like here: https://stackoverflow.com/a/73029625/8816585
<script setup>
import { ref } from "vue"
import Hehe from "#/components/Hehe.vue"
import Nice from "#/components/Nice.vue"
const boolean = ref(true)
</script>
<template>
<component :is="boolean ? Hehe : Nice" />
<button #click="boolean = !boolean">toggle</button>
</template>

Related

Show on click / hide on blur wrapper component

I have several widgets that I'd like to toggle on click/blur/submit.
Let's take a simple example with an input (Vue 2 style)
Input.vue
<template>
<input
ref="input"
:value="value"
#input="input"
#blur="input"
#keyup.escape="close"
#keyup.enter="input"
/>
</template>
<script>
export default {
props: ['value'],
methods: {
input() {
this.$emit("input", this.$refs.text.value);
this.close();
},
close() {
this.$emit("close");
},
}
}
</script>
ToggleWrapper.vue
<template>
<div #click="open = true">
<div v-if="open">
<slot #close="open = false"></slot> <!-- Attempt to intercept the close event -->
</div>
</div>
</template>
<script>
export default {
data() {
return {
open: false,
}
},
}
</script>
Final usage:
<ToggleWrapper>
<Input v-model="myText" #submit="updateMyText" />
</ToggleWrapper>
So when I click on ToggleWrapper it appears, but if I close it, it doesn't disappear because it's not getting the close event.
Should I use scoped events ?
How can I intercept the close event by adding the less possible markup on the final usage ?
I think it makes sense to use a scoped slot to do this. But you can also try this kind of solution.
Input.vue
<template>
<input
ref="input"
:value="value"
#input="input"
#blur="input"
#keyup.escape="close"
#keyup.enter="input"
/>
</template>
<script>
export default {
props: ['value'],
methods: {
input() {
this.$emit("input", this.$refs.text.value);
this.close();
},
close() {
this.$parent.$emit('close-toggle')
},
}
}
</script>
ToggleWrapper.vue
<template>
<div #click="open = true">
Click
<div v-if="open">
<slot></slot> <!-- Attempt to intercept the close event -->
</div>
</div>
</template>
<script>
export default {
data() {
return {
open: false,
}
},
created() {
this.$on('close-toggle', function () {
this.open = false
})
}
}
</script>
In a Vue3 style, I would use provide and inject (dependency injection). This solution leaves the final markup very light and you still have a lot of control, see it below :
Final usage :
<script setup>
import { ref } from 'vue'
import ToggleWrapper from './ToggleWrapper.vue'
import Input from './Input.vue'
const myText = ref('hi')
const updateMyText = ($event) => {
myText.value = $event
}
</script>
<template>
<ToggleWrapper>
<Input :value="myText" #submit="updateMyText" />
</ToggleWrapper>
<p>value : {{myText}}</p>
</template>
ToggleWrapper.vue
<template>
<div #click="open = true">
<div v-if="open">
<slot></slot>
</div>
<span v-else>Open</span>
</div>
</template>
<script setup>
import { provide, inject, ref } from 'vue'
const open = ref(false)
provide('methods', {
close: () => open.value = false
})
</script>
Input.vue
<template>
<input
:value="value"
#input="input"
#blur="close"
#keyup.escape="close"
#keyup.enter="submit"
/>
</template>
<script setup>
import { inject, ref } from 'vue'
const props = defineProps(['value'])
const emit = defineEmits(['close', 'input', 'submit'])
const methods = inject('methods')
const value = ref(props.value)
const input = ($event) => {
value.value = $event.target.value
emit("input", $event.target.value);
}
const close = () => {
methods.close()
emit('close')
}
const submit = () => {
emit('submit', value.value)
close()
}
</script>
See it working here

How do i get a Vue Compsable to only work with a single target item in a list

I have a composable that takes a function and calls the function upon the long-press of a component that uses it(the composable). however, if I use it in a list of components, it seems to call the function on all the components instead of only the component/item I long-pressed in the list. How do I make it so that the function is only called on the target component/item?
The composable(/composables/useLongPress)
import { ref, onMounted, onUnmounted } from 'vue';
const longPress = (binding) => {
const isHeld = ref(false);
const activeHoldTimer = ref(null);
const onHoldStart = () => {
isHeld.value = true;
activeHoldTimer.value = setTimeout(() => {
if (isHeld.value) {
binding();
}
}, 1000);
};
const onHoldEnd = () => {
isHeld.value = false;
if (activeHoldTimer.value) {
clearTimeout(activeHoldTimer.value);
}
};
onMounted(() => {
window.addEventListener('mousedown', onHoldStart);
window.addEventListener('touchstart', onHoldStart);
window.addEventListener('click', onHoldEnd);
window.addEventListener('mouseout', onHoldEnd);
window.addEventListener('touchend', onHoldEnd);
window.addEventListener('touchcancel', onHoldEnd);
});
onUnmounted(() => {
window.removeEventListener('mousedown', onHoldStart);
window.removeEventListener('touchstart', onHoldStart);
window.removeEventListener('click', onHoldEnd);
window.removeEventListener('mouseout', onHoldEnd);
window.removeEventListener('touchend', onHoldEnd);
window.removeEventListener('touchcancel', onHoldEnd);
});
};
export default longPress;
The child component/item (/InvoiceListItem.vue)
<template>
<div class="grid grid-cols-2 py-4">
<div>
<p class="text-lg font-medium">{{ invoice.client }}</p>
<p class="mt-2 text-xs font-light text-gray-500">Due {{ invoice.due }}</p>
</div>
<div class="flex flex-col items-end">
<p class="text-lg font-bold">${{ invoice.amount }}</p>
<base-pill
:variant="invoice.status"
:label="capitalize(invoice.status)"
class="mt-2"
/>
</div>
</div>
</template>
<script setup>
import BasePill from '../../BasePill.vue';
import { capitalize } from '../../../utils';
import useLongPress from '../../../composables/useLongPress';
const props = defineProps({
invoice: {
type: Object,
required: true,
},
});
const log = () => console.log(props.invoice);
useLongPress(log);
</script>
<style lang="postcss" scoped></style>
The parent/list of components (/InvoiceList.vue)
<template>
<div class="divide-y">
<div v-for="(invoice, index) in invoices" :key="index">
<invoice-list-item :invoice="invoice" />
</div>
</div>
</template>
<script setup>
import InvoiceListItem from './InvoiceListItem.vue';
defineProps({
invoices: {
type: Array,
default: () => [],
},
});
</script>
<style lang="scss" scoped></style>
when I long press on one invoice item I am supposed to log the value of the one item but instead, I get the value of all items in the list

How to use "this" in Vue 3

I'm working on a component in Vue3/TypeScript. I'm relatively new to Vue.
I'm dynamically creating a div. I want to be able to delete it when a user clicks on the "x" button. This is the "closeStatus" method.
In JavaScript I can pass this, but Vue hates it.
If anyone can help.
Thanks.
Here is the code:
<template>
<div>
<button #click="addStatus()" >Add Status</button>
<div id="slide" class="slide-modal">
<div class="clear-notifications" #click='clearAllNotifications()'>Clear all notifications</div>
<div class="status-div">
<div>11:00 Booking requested</div>
<div #click="closeStatus(this)"><img src="../assets/cross.svg" /></div>
</div>
<div class="status-div">
<div>11:30 Booking requested</div>
<div #click="closeStatus(this)"><img src="../assets/cross.svg" /></div>
</div>
</div>
</div>
</template>
<script lang="ts">
export default {
name: 'SlideModal',
methods: {
clearAllNotifications() {
const allNotifications = document.querySelectorAll(".status-div");
for(let x=0;x<allNotifications.length;x++) {
allNotifications[x].remove(); //removes all the child elements.
}
document.getElementById('slide').style.visibility='hidden'; //This should be Vue reactive.
},
addStatus() {
document.getElementById('slide').style.visibility='visible'; //This should be Vue reactive.
const statusMessage = "Another Message"
var div = document.createElement('div');
div.setAttribute('class', 'status-div');
div.innerHTML = '<div>' + statusMessage + '</div><div onclick="closeStatus(this)"><img src="/src/assets/cross.svg" /></div>'
document.getElementById('slide').appendChild(div);
},
closeStatus(elm: any) {
elm.parentNode.remove();
const allNotifications = document.querySelectorAll(".status-div");
if(allNotifications.length == 0 ) {
document.getElementById('slide').style.visibility='hidden'; //This should be Vue reactive.
}
}
}
}
</script>
Code is javascript DOM management stuff and far from Vue style.
Vue manages DOM with variables and that is what makes javascript frameworks awesome.
Let me first share the modified code.
<template>
<div>
<button #click="addStatus()" >Add Status</button>
<div id="slide" class="slide-modal" v-show="visibleSlide">
<div class="clear-notifications" #click='clearAllNotifications()'>Clear all notifications</div>
<div class="status-div">
<div>11:00 Booking requested</div>
<div #click="closeStatus(this)"><img src="../assets/cross.svg" /></div>
</div>
<div class="status-div">
<div>11:30 Booking requested</div>
<div #click="closeStatus(this)"><img src="../assets/cross.svg" /></div>
</div>
</div>
</div>
</template>
<script lang="ts">
export default {
name: 'SlideModal',
data() {
return {
visibleSlide: true
}
},
methods: {
clearAllNotifications() {
const allNotifications = document.querySelectorAll(".status-div");
for(let x = 0; x < allNotifications.length; x++) {
allNotifications[x].remove(); //removes all the child elements.
}
this.visibleSlide = false
},
addStatus() {
this.visibleSlide = true;
const statusMessage = "Another Message";
var div = document.createElement('div');
div.setAttribute('class', 'status-div');
div.innerHTML = '<div>' + statusMessage + '</div><div onclick="closeStatus(this)"><img src="/src/assets/cross.svg" /></div>'
document.getElementById('slide').appendChild(div);
},
closeStatus(elm: any) {
elm.parentNode.remove();
const allNotifications = document.querySelectorAll(".status-div");
if(allNotifications.length == 0 ) {
this.visibleSlide = false
}
}
}
}
</script>
I have just updated what is commented: "//This should be Vue reactive."
But it is still not benefiting from Vue framework.
Here is my whole idea of Vue code
<template>
<div>
<button #click="addStatus()" >Add Status</button>
<div id="slide" class="slide-modal" v-if="statuses.length">
<div class="clear-notifications" #click='clearAllNotifications()'>Clear all notifications</div>
<div v-for="status in statuses" :key="status.id" class="status-div">
<div>{{ status.text }}</div>
<div #click="closeStatus(status)"><img src="../assets/cross.svg" /></div>
</div>
</div>
</div>
</template>
<script lang="ts">
export default {
name: 'SlideModal',
data() {
return {
statuses: [
{
id: 1,
text: '11:00 Booking requested'
}, {
id: 2,
text: '11:30 Booking requested'
}
]
}
},
methods: {
clearAllNotifications() {
this.statuses = []
},
addStatus() {
const statusMessage = "Another Message";
this.statuses.push({
id: this.statuses.length + 1,
text: statusMessage
})
},
closeStatus(elm: any) {
const index = this.statuses.map((status) => status.id).indexOf(elm.id);
this.statuses.splice(index, 1);
}
}
}
</script>
As you can see, DOM elements are managed by variables and their operations instead of DOM management methods.
Hope this is helpful

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

VueJS display dynamic modal component

I have posts and replys s.t. replies belong to posts via the attribute reply.posts_id.
I am attempting to show the reply form as a modal for the user to enter a reply. However, I want to create a generic Modal component that I can use everywhere with content that is specified in another component built for a specific context.
Reply to post is the first place I woul like this to work.
Currently, the Vuex correctly returns Modal visible:true when the reply button is clicked, but the modal does not render and I get the error message showing that the Modal component is not found:
Unknown custom element: <ModalReplyForm> - did you register the component correctly? For recursive components, make sure to provide the "name" option.
I am using vuex to manage the visibility of the modal. Here are the relevant files:
store.js:
import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios'
...
Vue.use(Vuex)
export default new Vuex.Store({
state: {
status: '',
...
modalVisible: false,
modalComponent: null
},
mutations: {
...
showModal(state, componentName) {
console.log('showing the modal')
state.modalVisible = true;
state.modalComponent = componentName;
},
hideModal(state) {
console.log('hiding the modal')
state.modalVisible = false;
}
},
actions: {
...
}
},
getters: {
isAuthenticated: state => !!state.user,
authStatus: state => state.status,
user: state => state.user,
token: state => state.token,
posts: state => {
return state.posts;
}
...
}
})
App.vue
<template>
<div id="app">
<app-modal></app-modal>
<NavigationBar />
<div class="container mt-20">
<router-view />
</div>
<vue-snotify></vue-snotify>
</div>
</template>
<script>
import AppModal from '#/components/global/AppModal';
import NavigationBar from '#/components/layout/NavigationBar'
export default {
name: "App",
components: {
AppModal,
NavigationBar
}
};
</script>
<style>
body {
background-color: #f7f7f7;
}
.is-danger {
color: #9f3a38;
}
</style>
Post.vue (houses the button to call the reply modal):
<template>
<div class="row ui dividing header news">
<!-- Label -->
<div class="m-1 col-md-2 ui image justify-content-center align-self-center">
<img v-if="post.avatar_url" :src="post.avatar_url" class="mini rounded"/>
<v-gravatar v-else :email="post.email" class="mini thumbnail rounded image rounded-circle z-depth-1-half"/>
</div>
<!-- Excerpt -->
<div class="col-md-9 excerpt">
...
<!-- Feed footer -->
<div class="feed-footer row">
<div class="small"> {{ post.created_at | timeAgo }}</div>
<button type="button" flat color="green" #click="showModal('ModalReplyForm')">
<i class="fa fa-reply" ></i>
...
<div v-show="postOwner(post)" class="">
<button type="button" flat color="grey" #click="deletePost(post.id)">
<i class="fa fa-trash " ></i>
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapMutations } from 'vuex';
import PostsService from '../../services/PostsService'
import RepliesService from '../../services/RepliesService'
import Replies from '#/components/Reply/Replies'
import ReplyForm from '#/components/Reply/ReplyForm'
export default {
name: "Post",
props: {
post: {
type: Object,
required: true
}
},
components: {
Replies,
ReplyForm
},
computed: {
me() {
return this.$store.getters.user
}
},
methods: {
...mapMutations(['showModal']),
...
}
};
</script>
AppModal.vue - generic Modal component
<template>
<div class="c-appModal">
<div class="c-appModal__overlay" v-if="visible"></div>
<div class="c-appModal__content" v-if="visible" #click.self="hideModal"></div>
<div class="c-appModal__innerContent">
<component :is="component"></component>
</div>
</div>
</template>
<script>
import Vue from 'vue';
import { mapState, mapMutations } from 'vuex';
export default {
name: 'AppModal',
data() {
return {
component: null
}
},
computed: {
...mapState({
visible: 'modalVisible',
modalComponent: 'modalComponent'
}),
},
methods: {
...mapMutations(['hideModal'])
},
watch: {
modalComponent(componentName) {
if (!componentName) return;
Vue.component(componentName, () => import(`#/components/modals/${componentName}`));
this.component = componentName;
}
},
created() {
const escapeHandler = (e) => {
if (e.key === 'Escape' && this.visible) {
this.hideModal();
}
};
document.addEventListener('keydown', escapeHandler);
this.$once('hook:destroyed', () => {
document.removeEventListener('keydown', escapeHandler);
});
},
};
</script>
ModalReplyForm - specific reply modal content
<template>
<div>
<div class="c-modalReply">
<div>
<label for="reply">Your comment</label>
<div class="field">
<textarea name="reply" v-model="reply" rows="2" placeholder="Compose reply"></textarea>
</div>
</div>
<button class="c-modalReply__cancel" #click="hideModal">Cancel</button>
<button class="c-modalReply__post" :disabled="!isFormValid" #click="createReply">Reply</button>
</div>
</div>
</template>
<script>
import RepliesService from '#/services/RepliesService'
import { mapMutations } from 'vuex';
export default {
name: "ModalReplyForm",
// props: {
// post: {
// type: Object,
// required: true
// }
// },
data() {
return {
reply: ""
};
},
computed: {
isFormValid() {
return !!this.reply;
},
currentGroup() {
return this.$store.getters.currentPost;
}
},
methods: {
...mapMutations([
'hideModal'
]),
async createReply () {
let result = await RepliesService.addReply({
reply: {
body: this.reply,
postId: this.post.id
}
});
this.$emit("reply-created");
this.hideModal();
}
}
};
</script>
Unknown custom element: - did you register the
component correctly? For recursive components, make sure to provide
the "name" option.
This message says that you never imported/defined ModalReplyForm, which you have not.
In my own generic modal, I ended up having to import all the components that might appear within the modal itself.
If you add a:
import ModalReportForm from ...
and a:
components: {
ModalReplyForm
}
to AppModal.vue, the modal should then do what you expect.