Dynamic rendering of popup component vuejs - javascript

I would like to create a VueJS app and I have a problem. I would like to be able to display some components and when I will right click on it, a popup will be displayed. The problem is that the popup will be different for every component. I saw something with the component id but I don't know if It can be answer to my problem.
Example of code :
Vue.component('component', {
template: `<button v-on:click="showPopup()">Open popup</button>`
})
Vue.component('popup1', {
template: '<div>Some complex features ... </button>'
})
Vue.component('popup2', {
template: '<div>Another complex features ... </button>'
})
The idea here is that the component 'component' don't really know which popup to display. It will be the function showPopup that will know the popup.

You can send a simple data to know which popup should be show to your client,
for example:
in App.vue setup() section you have this :
const popup = reactive({
type: "none",
data: "hello world"
});
const popupToggle = ref(false);
function showPopup(type, data){
popup.type = type;
popup.data = data;
popupToggle.value = true;
}
function closePopup(){
popup.type = "none";
popup.data = "empty";
popupToggle.value = false;
}
and provide your functions into your project with :
provide("popupFunctions", {showPopup, closePopup});
and inject provided functions in other child documents with :
const {showPopup, closePopup} = inject("popupFunctions");
now all you need is call the functions which named showPopup and closePopup to change popup variable which you created before in your App.vue and check the popup type to show the targeted component as a popup to your client
Something like this in <template> section in your App.vue :
<popup-component v-if="popupToggle">
<popup-msgbox v-if="popup.type === 'msgBox'" :popup-data="popup.data" />
<popup-formbox v-else-if="popup.type === 'formBox'" :popup-data="popup.data" />
<popup-errorbox v-else-if="popup.type === 'errBox'" :popup-data="popup.data" />
</popup-component>
of course you should import these components and other required things in your project as you know, and i just tried to clear the solve way for you.
I hope my answer be clear to you and help you solve your problem.

Here's a working example: https://codesandbox.io/s/nervous-dew-kjb4ts
Step 1
Make a modal - see example.
We can use slots to put dynamic content, like other components in each instance of the modal, and named slots for multiple sections. We will control visibility in the outer component / the mixin.
<template>
<transition name="modal">
<div class="modal-header">
<slot name="header"> default header </slot>
</div>
<div class="modal-body">
<slot name="body"> default body </slot>
</div>
<slot name="footer">
Default Footer
<button class="modal-default-button" #click="$emit('close')">
🚫 Close
</button>
</slot>
</transition>
</template>
<script>
export default {
name: "Modal",
};
</script>
<style scoped>
// See link above for full styles
</style>
Step 2
Create a mixin that all components containing a modal can extend from. Here we'll put methods for opening, closing and anything else you need. Create a data attribute to indicate modal state for use with v-if, then add two methods for opening and closing.
import Modal from "#/components/Modal";
export default {
components: {
Modal
},
data: () => ({
modalState: false
}),
methods: {
openModal() {
this.modalState = true;
},
closeModal() {
this.modalState = false;
},
},
};
Step 3
Create your components, that extend from the mixin, and use the modal component with whatever content you like.
You can trigger right-clicks using: #mouseup.right
<template>
<div class="example-component comp1">
<h2>Component 1</h2>
<button #contextmenu.prevent
#mouseup.right="openModal()"
#click="tryRightClick()">
Open Component 1 Modal
</button>
<Modal v-if="modalState" #close="closeModal()">
<template v-slot:header>👉 Component 1 Modal</template>
<template v-slot:body>
Lorem ipsum
</template>
</Modal>
</div>
</template>
<script>
import modalMixin from "#/mixins/component-modal-mixin";
export default {
mixins: [modalMixin],
};
</script>
Step 4
Finally, just import your components.
<template>
<div id="app">
<h3>StackOverflow Answer for Terbah Dorian</h3>
<i>Example of separate components opening separate modals</i>
<Component1 />
<Component2 />
<Component3 />
</div>
</template>
<script>
import Component1 from "#/components/Component1";
import Component2 from "#/components/Component2";
import Component3 from "#/components/Component3";
export default {
name: "App",
components: {
Component1,
Component2,
Component3,
},
};
</script>
Hope that helps :)
If it did, then an upvote would be appreciated!

https://codesandbox.io/s/laughing-shape-18dnqq?file=/src/App.vue
I made a working sample to display dynamic popovers by hovering the buttons. You can use Slots in Vue components.
Hope this helps you.

Related

Conditional template rendering with props in Vue3?

I'm trying to make a toggle button so that a hamburger menu opens when clicked.
I made the boolean "clicked" property in "App.vue", passed it down to "Navbar.vue", and now I want to be able to click in the navbar to toggle the "clicked" property to "true" or "false" to make the backdrop and drawer show or not show.
I tried to use an "emit", and it seems to work, but the template isn't responding to the "clicked" variable and is showing even though it's false.
In the code below, what part did I get wrong? How do you implement conditional rendering with props? Can someone help?
App.vue
<template>
<NavBar :clicked="clicked" #toggleDrawer="toggleMenu()" />
<BackDrop :clicked="clicked" />
<SideDrawer :clicked="clicked" />
<router-view></router-view>
</template>
<script>
export default {
name: "App",
components: { NavBar, BackDrop, SideDrawer },
setup() {
const clicked = ref(false);
const toggleMenu = () => {
clicked.value = !clicked.value;
};
return { clicked, toggleMenu };
},
};
</script>
NavBar.vue
<template>
<nav class="navbar">
/* MORE CODE */
<div class="hamburger_menu" #click="toggleEvent">
<div></div>
<div></div>
<div></div>
</div>
</nav>
</template>
<script setup>
import { defineEmits, defineProps } from "vue";
const props = defineProps({
clicked: Boolean,
});
const emit = defineEmits(["toggleDrawer"]);
const toggleEvent = () => {
console.log("toggleEvent running");
emit("toggleDrawer", !props.clicked);
};
</script>
Backdrop.vue
<template v-if="props.clicked">
<div class="backdrop"></div>
</template>
<script setup>
import { defineProps } from "vue";
// eslint-disable-next-line no-unused-vars
const props = defineProps({
clicked: Boolean,
});
</script>
SideDrawer.vue
<template v-if="props.clicked">
<div class="sidedrawer"></div>
</template>
<script setup>
import { defineProps } from "vue";
const props = defineProps({
clicked: Boolean,
});
</script>
Am I passing in the prop wrong? Does "props.clicked" not work in "v-if"'s or templates? How should I implement the "v-if" with the "clicked" property I have?
As #neha-soni said,
After running the code, it is working fine
Vue recommends to use kebab-cased event listeners in templates. Your toggleDrawer will auto-converted into kebab case when you use it in the parent component. So In app.vue you can use it like #toggle-drawer,
<NavBar :clicked="clicked" #toggle-drawer="toggleMenu()" />
From vue doc link
event names provide an automatic case transformation. Notice we emitted a camelCase event, but can listen for it using a kebab-cased listener in the parent. As with props casing, we recommend using kebab-cased event listeners in templates.
After running the code, it is working fine. I have a few feedbacks to remove unnecessary code which is creating confusion and then you can see its working.
Because props are immutable (read-only) in the child component that means their value will not be changed so there is no point to pass props value back (by doing emit("toggleDrawer", !props.clicked)) to the parent because the parent already has their original status.
Another point is, you are passing the data (props data) from the event by doing emit("toggleDrawer", !props.clicked) but not using it when calling the function #toggleDrawer="toggleMenu()" in App.vue, so better to remove this data passing code.
The clicked property is updating in the parent (App.vue) as well as inside the child components. Just console and print the clicked property in the child and parent template like {{ clicked }} at the top and you can see the updated status-
const toggleMenu = () => {
console.log('Before______', clicked.value)
clicked.value = !clicked.value;
console.log('After______', clicked.value)
};

Communication between children components: Show / Hide shopping cart using emit from a button in other component

I’m working on a shopping cart using vue3 with inertiajs and Laravel and page layout demands to have a main menu with a bag button which on click fires showing and hiding cart event.
So as menu and cart are children component from main page, I guess that the goal is by using communication between components.
I’ve read emit documentation and I can get it working from parent to child and from child to parent, but I can’t achieve the communication between both children components.
I have the following code:
Parent
<Cart :isVisible="showCart" #hideCart="showCart = false" />
<script>
export default {
data() {
return {
showCart: false,
}
},
methods: {
toggleCart() {
this.showCart = !this.showCart
},
}
}
</script>
Menu
<button #click="$emit('toggleCart')"><Icon name="bag" class="w-4" /></button>
Cart
<template>
<div v-if="isVisible">
<h3 class="flex justify-between">
<span class="font-bold">My cart</span>
<button #click="$emit('toggleCart')" class="text-xl">×</button>
</h3>
...html code for cart
</div>
</template>
<script setup>
import { Link } from "#inertiajs/inertia-vue3";
const props = defineProps(["isVisible", "cart"]);
const emit = defineEmits(["toggleCart"]);
</script>
$emit doesn't call a method in the parent, you have to handle the $emitted event yourself, usually by calling a method. So in your parent component you should have:
<template>
<Cart :isVisible="showCart" #toggle-cart="toggleCart"..... />
<Menu #toggle-cart="toggleCart" ..... />
</template>
<script>
//...
methods: {
toggleCart() {
this.showCart = !this.showCart
},
//...
</script>
The #toggle-cart= is referring to the emitted event, the "toggleCart" is referring to the method in your parent component, but there's no need for them to have the same name. You could have #toggle-cart="handleCartToggle" as long as handleCartToggle is your method name.
(Note: The kebab-case #toggle-cart instead of #toggleCart is how Vue recommends you refer to events in the template, to align with HTML attributes. Still use toggleCart in the Cart component.

How to display an element in Vue component only after NProgress.done()

For displaying the loading status in a VueJS application I use the library NProgress. It works well and shows the loading bar and the spinning wheel. However the HTML content of the page is already rendered and displayed. I'd like to hide certain parts of the page while the request is running.
Is there any possibility to check programmatically for NProgress.done() and display the contents after it has been called?
I'd like something like this:
<template>
<div>
<NavBar />
<div v-show="check here for NProgress.done()">
<p>Here are all the nice things with placeholders for the data from the API.</p>
</div>
</div>
</template>
<script>
import NavBar from '#/components/NavBar';
export default {
components: {
NavBar
}
}
</script>
The part "check here for NProgress.done()" is what I don't know how to solve.
Looking through the documentation of NProgress, it looks like it exposes a ".status", which returns the value of the current loader, but returns a "null" when the loader isn't "started".
<template>
<div>
<div v-show="state.status == null">
<p>
Here are all the nice things with placeholders for the data from the
API.
</p>
</div>
</div>
</template>
<script>
import Vue from "vue";
import NProgress from "nprogress";
import "nprogress/nprogress.css";
const state = Vue.observable(NProgress);
export default {
data: () => ({ state }),
mounted: function () {
NProgress.start(); // Start the bar loading
setTimeout(() => { // Perform your async workloads
NProgress.done(); // .done() to finish the loading
}, 5000);
},
};
</script>
You'd need to make NProgress reactive, so you can just use Vue.observable(state).

How to Access Component HTML output in Vue outside the template tag?

I know that we can simply show the component output with <ComponentName/> inside the template,
but how do we access ComponentName html output outside the template like in data, methods, or during mounted
e.g. components/Test.vue
<template>
<div>I'm a test</div>
</template>
in another vue file pages/ViewTest.vue
import Test from '~/components/Test.vue'
export default {
components: {Test},
data() {
return {
test: Test
}
},
mounted: function() {
console.log( Test ) // Output is Test Component Object
console.log( this.test ) // Output is Test Component Object
}
}
The object from console log output seems to contain a lot of information and I can even see a render property from the object although when I try console.log( Test.render() ) its giving me error
So My question is how can I get the <div>I'm a test</div> from outside the template?
Appreciate any help or guidance
EDIT
I'm using vue-material-design-icons package for generating different svg icons,
and I can use it like below
<template>
<MapMarkerRadius/>
</template>
<script>
import MapMarkerRadius from 'vue-material-design-icons/MapMarkerRadius'
export default {
components: {MapMarkerRadius}
}
</script>
Now here's my main issue,
I have this component that generates an html
<template>
<div :class="'card'">
<div v-if="title" :class="'card-title'">
{{ title }}
</div>
<div :class="'card-content'">
<slot />
</div>
</div>
</template>
<script>
export default {
name: 'card',
props: {
title: {},
}
};
</script>
Then I'm using that card component like this on a different vue file
<template>
<card :title="'Title ' + MapMarkerRadius">
Test Content
</card>
</template>
<script>
import card from '~/components/Card'
import MapMarkerRadius from 'vue-material-design-icons/MapMarkerRadius'
export default {
components: {card, MapMarkerRadius}
};
</script>
and my problem here is that the output of the card title is Title [object]
Try to use ref in the root of the Test component like :
<template>
<div ref="test">I'm a test</div>
</template>
in other component do :
mounted: function() {
console.log( this.$refs.test )
}
No need to import the component.
The repo that you are using are single-file components that generates html through a single tag, so using
import MapMarkerRadius from 'vue-material-design-icons/MapMarkerRadius'
will enable you to use it in template as <map-marker-radius/>
That is why appending the string title and an object like "My Icon"+MapMarkerRadius will return the literal [object] as you've seen: "My Icon [object]"
You have 3 options to go through what you want:
Search for other repos that enable you to use easily material icons in other means;
You have access to the card component? You can use the class names of this repo instead rather than the svg version or the component itself: https://github.com/robcresswell/vue-material-design-icons/issues/12, add the class names to the props and add it to your component:
<card :title="'Title'" :icon_class="map-marker-radius">
Test Content
</card>
<div v-if="title" :class="'card-title'">
{{ title }} <div :class="icon_class"></div>
</div>
props: {
title: {},
icon_class: '',
}
You can use the MapMarkerRadius component directly in card component but only appears when you pass a certain criteria on the card component, such like:
main.vue
<template>
<card :title="'Title'" :icon="true" :icon_typename="'map-marker-radius'">
Test Content
</card>
</template>
<script>
import card from '~/components/Card'
export default {
components: {card}
};
</script>
with icon_typename as any name/keyword you'd like to use.
card.vue
<template>
<div :class="'card'">
<div v-if="title" :class="'card-title'">
{{ title }} <span v-if="icon_mmr"><map-marker-radius/></span>
</div>
<div :class="'card-content'">
<slot />
</div>
</div>
</template>
<script>
import MapMarkerRadius from 'vue-material-design-icons/MapMarkerRadius'
export default {
name: 'card',
props: {
title: {},
icon: { default: false },
icon_typename: '',
icon_mmr: false,
},
mounted(){
if (this.icon && this.icon_typename === 'map-marker-radius') this.icon_mmr = true
},
components: { MapMarkerRadius },
};
</script>
The code is far from perfect but you can go from there to optimize further.

Change a property's value in one component from within another component

I'm trying to wrap my head around hoe Vue.js works, reading lots of documents and tutorials and taking some pluralsight classes. I have a very basic website UI up and running. Here's the App.vue (which I'm using kinda as a master page).
(To make reading this easier and faster, look for this comment: This is the part you should pay attention to)...
<template>
<div id="app">
<div>
<div>
<CommandBar />
</div>
<div>
<Navigation />
</div>
</div>
<div id="lowerContent">
<!-- This is the part you should pay attention to -->
<template v-if="showLeftContent">
<div id="leftPane">
<div id="leftContent">
<router-view name="LeftSideBar"></router-view>
</div>
</div>
</template>
<!-- // This is the part you should pay attention to -->
<div id="mainPane">
<div id="mainContent">
<router-view name="MainContent"></router-view>
</div>
</div>
</div>
</div>
</template>
And then in the same App.vue file, here's the script portion
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import CommandBar from './components/CommandBar.vue';
import Navigation from './components/Navigation.vue';
#Component({
components: {
CommandBar,
Navigation,
}
})
export default class App extends Vue {
data() {
return {
showLeftContent: true // <--- This is the part you should pay attention to
}
}
}
</script>
Ok, so the idea is, one some pages I want to show a left sidebar, but on other pages I don't. That's why that div is wrapped in <template v-if="showLeftContent">.
Then with the named <router-view>'s I can control which components get loaded into them in the `router\index.ts\ file. The routes look like this:
{
path: '/home',
name: 'Home',
components: {
default: Home,
MainContent: Home, // load the Home compliment the main content
LeftSideBar: UserSearch // load the UserSearch component in the left side bar area
}
},
So far so good! But here's the kicker. Some pages won't have a left side bar, and on those pages, I want to change showLeftContent from true to false. That's the part I can't figure out.
Let's say we have a "Notes" component that looks like this.
<template>
<div class="notes">
Notes
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
#Component
export default class Notes extends Vue {
data() {
return {
showLeftContent: false // DOES NOT WORK
}
}
}
</script>
Obviously, I'm not handling showLeftContent properly here. It would seem as if the properties in data are scoped only to that component, which I understand. I'm just not finding anything on how I can set a data property in the App component and then change it in a child component when that child is loaded through a router-view.
Thanks!
EDIT:
I changed the script section of the Notes component from:
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
#Component
export default class Notes extends Vue {
data() {
return {
showLeftContent: false // DOES NOT WORK
}
}
}
</script>
to:
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
#Component
export default class Notes extends Vue {
mounted() {
this.$root.$data.showLeftContent = false;
}
}
</script>
And while that didn't cause any compile or runtime errors, it also didn't have the desired effect. On Notes, the left side bar still shows.
EDIT 2:
If I put an alert in the script section of the Notes component:
export default class Notes extends Vue {
mounted() {
alert(this.$root.$data.showLeftContent);
//this.$root.$data.showLeftContent = false;
}
}
The alert does not pop until I click on "Notes" in the navigation. But, the value is "undefined".
EDIT 3:
Struggling with the syntax here (keep in mind this is TypeScript, which I don't know very well!!)
Edit 4:
Inching along!
export default class App extends Vue {
data() {
return {
showLeftContent: true
}
}
leftContent(value: boolean) {
alert('clicked');
this.$root.$emit('left-content', value);
}
}
This does not result in any errors, but it also doesn't work. The event never gets fired. I'm going to try putting it in the Navigation component and see if that works.
As it says on #lukebearden answer you can use the emit event to pass true/false to the main App component on router-link click.
Assuming your Navigation component looks like below, you can do something like that:
#Navigation.vue
<template>
<div>
<router-link to="/home" #click.native="leftContent(true)">Home</router-link> -
<router-link to="/notes" #click.native="leftContent(false)">Notes</router-link>
</div>
</template>
<script>
export default {
methods: {
leftContent(value) {
this.$emit('left-content', value)
}
}
}
</script>
And in your main App you listen the emit on Navigation:
<template>
<div id="app">
<div>
<Navigation #left-content="leftContent" />
</div>
<div id="lowerContent">
<template v-if="showLeftContent">
//...
</template>
<div id="mainPane">
//...
</div>
</div>
</div>
</template>
<script>
//...
data() {
return {
showLeftContent: true
}
},
methods: {
leftContent(value) {
this.showLeftContent = value
}
}
};
</script>
A basic approach in a parent-child component relationship is to emit events from the child and then listen and handle that event in the parent component.
However, I'm not sure that approach works when working with the router-view. This person solved it by watching the $route attribute for changes. https://forum.vuejs.org/t/emitting-events-from-vue-router/10136/6
You might also want to look into creating a simple event bus using a vue instance, or using vuex.
If you'd like to access the data property (or props, options etc) of the root instance, you can use this.$root.$data. (Check Vue Guide: Handling Edge)
For your codes, you can change this.$root.$data.showLeftContent to true/false in the hook=mounted of other Components, then when Vue creates instances for those components, it will show/hide the left side panel relevantly.
Below is one demo:
Vue.config.productionTip = false
Vue.component('child', {
template: `<div :style="{'background-color':color}" style="padding: 10px">
Reach to root: <button #click="changeRootData()">Click me!</button>
<hr>
<slot></slot>
</div>`,
props: ['color'],
methods: {
changeRootData() {
this.$root.$data.testValue += ' :) '
}
}
})
new Vue({
el: '#app',
data() {
return {
testValue: 'Puss In Boots'
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
<h2>{{testValue}}</h2>
<child color="red"><child color="gray"><child color="green"></child></child></child>
</div>

Categories

Resources