Conditional template rendering with props in Vue3? - javascript

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)
};

Related

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.

Child emit does not update v-model prop Array of parent

I'm using this parent component:
<template>
<div id="cms-custom-editor" class="cms-custom-editor">
<cms-editor-toolbar v-model:toggles="state.toggles"/>
<div class="content">Toggle Buttons: {{ state.toggles }}</div>
</div>
</template>
<script setup>
import CmsEditorToolbar from './cms-editor-toolbar.vue'
import {reactive, ref} from "vue";
const state = reactive({
toggles: [],
})
</script>
to pass down state.toggles as a prop to the cms-editor-toolbar child component:
<template>
<div id="cms-editor-toolbar" class="cms-editor-toolbar">
<button #click="toggleButton"></button>
</div>
</template>
<script setup>
const props = defineProps({
toggles: Array,
})
const emit = defineEmits(['update:toggles'])
const toggleButton = () => {
// ... logic to determine toggleAction
console.log(toggleAction) // logs correct toggleAction
emit('update:toggles', toggleAction) //emits only first toggleAction and no other
}
</script>
For whatever reason emit('update:toggles', toggleAction) only emits the first toggleAction to the parent. For every other change, the state.toggles array is not updated.
From the answer in this question, I've tried using ref instead of reactive:
<template>
<div id="cms-custom-editor" class="cms-custom-editor">
<cms-editor-toolbar v-model:toggles="toggles"/>
<div class="content">Toggle Buttons: {{ toggles }}</div>
</div>
</template>
<script setup>
import CmsEditorToolbar from './cms-editor-toolbar.vue'
import {reactive, ref} from "vue";
const toggles = ref([1,2,3])
</script>
but it changes nothing. Again the array is only updated once and no consecutive action has any effect. How do I make this work?
Edit:
I've added additional props (not arrays) to sibling elements inside the child, which not only are emitted correctly, but also trigger all actions of the array emit. Basically it seems to save the array inside the child until the emit is triggered by a sibling emit, which sends the complete array to the parent. No idea how this is happening.
I've been using the wrong method to check if state.toggles is updated. Rendering it in a paragraph usually works, but in this case it doesn't seem to be rerendered for whatever reason.
Logging the array inside the parent like so:
setInterval(function(){
console.log(state.toggles)
}, 1000);
shows that it is indeed updating. The emits are working.

Dynamic rendering of popup component vuejs

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.

How to replace this.$parent.$emit in Vue 3?

I have migrated my application to Vue 3.
Now my linter shows a deprecation error, documented here: https://eslint.vuejs.org/rules/no-deprecated-events-api.html.
The documentation shows how to replace this.$emit with the mitt library, but it doesn't show how to replace this.$parent.$emit.
In your child component:
setup(props, { emit }) {
...
emit('yourEvent', yourDataIfYouHaveAny);
}
Your parent component:
<your-child #yourEvent="onYourEvent" />
...
onYourEvent(yourDataIfYouHaveAny) {
...
}
With script setup syntax, you can do:
<script setup>
const emit = defineEmits(['close', 'test'])
const handleClose = () => {
emit('close')
emit('test', { anything: 'yes' })
}
</script>
No need to import anything from 'vue'. defineEmits is included.
Read more here: https://learnvue.co/2020/01/4-vue3-composition-api-tips-you-should-know/
Due to the composition api, it allows you to use the $attrs inherited in each component to now fulfill this need.
I assume that you are using this.$parent.emit because you know the the child will always be part of the same parent. How do I simulate the above behavior with $attrs?
Lets say I have a table containing row components. However I wish to respond to row clicks in table's parent.
Table Definition
<template>
<row v-bind="$attrs" ></row>
</template>
Row Definition
<template name="row" :item="row" #click=onClick(row)>
Your Row
</template>
export default {
emits: {
row_clicked: () =>{
return true
}
},
onClick(rowData){
this.$emit('row_clicked',rowData)
}
}
Finally, a component containing your table definition, where you have a method to handle the click.
<table
#row_clicked=clicked()
>
</table
Your table component should effectively apply #row_clicked to the row component thus triggering when row emits the event.
There is similar way of doing it by using the context argument that is passed in second argument inside the child component (the one that will emit the event)
setup(props, context){
context.emit('myEventName')
}
...then emit it by calling the context.emit method within the setup method.
In your parent component you can listen to it using the handler like so:
<MyParentComponent #myEventName="handleMyEventName" />
Of course, in the setup method of the MyParentComponent component you can declare the handler like this
//within <script> tag of MyParentComponent
setup(props){
const handleMyEventName() => {
...
}
return { handleMyEventName }
}

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