I have a v-for loop that iterates through an array of meetings (meetings between sellers and potential buyers of used cars) and prints a card for each meeting, a basic display of who the meeting is with, what car it is about and the scheduled date. Now, I implemented a button that when clicked, opens a dialog with a Google Maps component that shows the marker for the agreed location of the meeting.
My problem is that no matter what card I click on, the dialog will always display the location of the LAST card, regardless of which has been clicked. I would think that since Im calling the component INSIDE the v-for loop it would pass props dynamically for each card, on each iteration, but that does not seem to be the case.
Here is the HTML:
<div
v-for="meeting in meetings"
:key="meeting.did"
class="col-12 col-md-6 col-lg-3 q-pa-md q-mx-xl"
>
<q-card class="my-card homeCard q-pa-md">
<q-dialog class="mapDialog flex column" v-model="mapDialog">
<MeetMapComponent
:key="componentKey"
:mapDiv="mapDiv"
:mapData="meeting.address"
:buyerName="meeting.name"
/>
</q-dialog>
<q-card-section
class="tipCardImage flex row justify-end"
:style="`background-image: url(${meeting.car.carImg})`"
>
<router-link
:to="`/user/meet/edit/${meeting.did}`"
style="text-decoration: none"
>
<q-icon
#click="fetchMeeting(meeting.did)"
name="fa-solid fa-pencil editNameIcon q-mb-sm q-ml-sm"
></q-icon>
</router-link>
<q-icon
name="fa-solid fa-trash editNameIcon q-mb-sm q-ml-sm"
#click="triggerDelete(meeting.did)"
></q-icon>
</q-card-section>
<q-card-section>
<div class="cardTitle">
<span>Encuentro Con</span> {{ truncateString(meeting.name, 30) }}
</div>
<div class="tipCardText">
<span>Agendado para el </span>
<p>{{ truncateString(meeting.date, 120) }}</p>
</div>
<div class="flex row justify-end">
<q-btn
#click="mapDialog = true"
class="text-white cardButton"
:class="{ cardButtonMobile: $q.screen.lt.md }"
>Ver Ubicación</q-btn
>
</div>
</q-card-section>
</q-card>
</div>
And here is the code for the MeetMapComponent:
<template>
<div class="meetMapContainer">
<div ref="mapDiv" style="width: 100%; height: 500px" />
<h5 class="text-center text-white">{{ props.mapData.address }}</h5>
</div>
</template>
<script setup>
import { ref } from "vue";
import { useAuthStore } from "stores/auth";
import { storeToRefs } from "pinia";
import { Loader } from "#googlemaps/js-api-loader";
const props = defineProps({
mapData: Object,
buyerName: String,
});
const GOOGLE_MAPS_API_KEY = "...";
const loader = new Loader({ apiKey: GOOGLE_MAPS_API_KEY });
const mapDiv = ref(null);
async function mapRender() {
await loader.load();
const map = new google.maps.Map(mapDiv.value, {
mapTypeId: "roadmap",
center: props.mapData.coordinates,
zoom: 13,
});
console.log(map);
new google.maps.Marker({
position: props.mapData.coordinates,
map,
title: `Encuentro con ${props.buyerName}`,
});
}
mapRender();
</script>
I will help you as much as I understand. You use the mapDialog variable to open the dialogue. But even if this variable is used in v-for, its reference does not change. For this reason, when you want to open a modal, all modals may be opened and the last one may appear because it is the last one opened. Please check the dom.
I think this method can solve the problem.
in script
const meetings = [
{
did: 'some value',
address: 'some address',
name: 'some name',
// add modal flag
showMapModal: false
}
]
template
<div
v-for="meeting in meetings"
:key="meeting.did"
class="col-12 col-md-6 col-lg-3 q-pa-md q-mx-xl"
>
<q-card class="my-card homeCard q-pa-md">
<q-dialog class="mapDialog flex column" v-model="meeting.showMapModal">
<MeetMapComponent
:key="componentKey"
:mapDiv="mapDiv"
:mapData="meeting.address"
:buyerName="meeting.name"
/>
</q-dialog>
</q-card>
</div>
Related
Im using Vue and Ionic, I dont know how to pass my lesson.id to the method openModal()
explanation: I have a card with my data - lesson data, where are also comments, when user clicks on them, modal window is open, I need to pass id of the lesson to my modal window as props, so I can display comments for the lesson.
<ion-content>
<div
class="lesson-card"
v-for="lesson in lesson.video_lessons"
:key="lesson"
>
<div class="lesson-content">
<h2>{{ lesson.content_description }}</h2>
<div class="tags">
<span v-for="tag in lesson.tags" :key="tag">
#{{ tag }}
</span>
</div>
<img
v-if="lesson.content_thumbnail"
:src="`${lesson.content_thumbnail}`"
alt="theme-img"
height="600"
/>
</div>
<div class="sidebar-icons">
bookmark
heart
<p>{{ lesson.likes }}</p>
<a #click="openModal">comments</a>
<p>lesson id: {{ lesson.id }}</p>
</div>
</div>
</ion-content>
this is my method
async openModal() {
const modal = await modalController.create({
component: CommentsModal,
componentProps: { id: 1 }, // i need to replace this 1 with id of the lesson
})
return modal.present()
},
In template, pass it like
<a #click="openModal(lession.id)">comments</a>
and in method
async openModal(payload) { // change added
const modal = await modalController.create({
component: CommentsModal,
componentProps: { id: payload}, // Change added
})
return modal.present()
},
apologies if that's too basic, but I'm stuck.
I have created an object in vue with three properties (slug, title and content). I successfully console.logged the object. How can I now use the object in my page in order to render its content?
There is no need for me to loop through the object, at it has only one item in it.
<template>
<div class="relative py-16 overflow-hidden bg-white">
<div class="relative px-4 sm:px-6 lg:px-8">
<div class="mx-auto text-lg max-w-prose">
<h1>
<span
class="block text-base font-semibold tracking-wide text-center text-indigo-600 uppercase"
>Hello</span
>
<span
class="block mt-2 text-3xl font-extrabold leading-8 tracking-tight text-center text-gray-900 sm:text-4xl"
>Here is the name</span
>
</h1>
<p class="mt-8 text-xl leading-8 text-gray-700"></p>
<div
v-bind="this.data.content"
class="text-lg font-medium leading-6 text-gray-900"
></div>
</div>
</div>
</div>
</template>
<script>
const Cosmic = require("cosmicjs");
const api = Cosmic();
const bucket = api.bucket({
slug: "((BUCKETNAME))",
read_key: "((KEY))",
});
const data = bucket
.getObject({
id: "((BUCKET ID))", // Object ID
props: "slug,title,content", // get only what you need
})
.then((data) => {
const about = data.objects;
console.log(data);
});
export default {
name: "data",
data() {
return {
data,
};
},
};
</script>
As other commenters have suggested, it would be useful to read the Vue syntax guide here https://v2.vuejs.org/v2/guide/syntax.html
But to answer your question with the most minimal of code changes, you'd want to move your data request to the lifecycle hook of your vue component.
<template>
<h1>{{ dataObjects.title }}</h1>
<p>{{ dataObjects.slug }}</p>
<p>{{ dataObjects.content }}</p>
</template>
<script>
export default {
name: "data",
data() {
return {
dataObjects: null,
};
},
mounted() {
bucket.getObject({
id: "((BUCKET ID))", // Object ID
props: "slug,title,content", // get only what you need
})
.then((data) => {
// Assign the return value to the dataObjects propery of the vue instance.
this.dataObjects = data;
});
}
};
</script>
In the template section, you can see that I've used curly braces to render the contents of dataObjects (I wasn't sure what structure your data is in).
You can also learn from examples on the Vue Cookbook site
I'm using 2 components in my page, one for a comment and inside the comment component is the comment reply component, I did it this way to add some functionality that I needed but now I don't know how to get the buttons in the components to do the stuff they did before I turned them into components. For example the edit function opens a dialog that is in the page where the comment component is located but now it doesn't do anything, same with the reply functionality.
How can I get this to work, or is there a way to keep those methods in the main page and not have to put them in the components? I've never made my own component before so I'm not sure how to do this.
This is the Comment component code, the editComment and openReply are methods from the main page
<template>
<div class="comment">
<el-card shadow="never" v-if="comment.parent_id === null">
<el-row v-if="comment.deleted === 0">
<el-row style="margin-bottom: 15px">
<el-col :span="20" v-if="forum.anonymous === 0">
<p class="comment-user-name">{{comment.user.name}} dice: </p>
</el-col>
<el-col :span="20" v-if="forum.anonymous === 1">
<p class="comment-user-name">Anónimo dice: </p>
</el-col>
<el-col :span="4">
<div style="text-align: end"
v-if="comment.user_id === $page.auth.user.auth.user.id">
<div v-if="canEdit" class="btn-link-edit action-button"
#click="editComment(comment)">
<i class="fas fa-pencil-alt"></i>
</div>
<div class="btn-link-delete action-button"
#click="remove(comment.id)">
<i class="fas fa-trash"></i>
</div>
<div class="btn-link-preview action-button"
#click="openReply(comment)">
<i class="fas fa-reply"></i>
</div>
</div>
<div style="text-align: end"
v-if="comment.user_id !== $page.auth.user.auth.user.id">
<div class="btn-link-preview action-button"
#click="openReply(comment)">
<i class="fas fa-reply"></i>
</div>
</div>
</el-col>
</el-row>
<el-row style="margin-top: 10px">
<p class="comment-comment">{{comment.comment}}</p>
</el-row>
<el-row class="fa-pull-right pb-1">
<p class="comment-user-time ">{{formatDate(comment.comment_time)}}</p>
</el-row>
</el-row>
<el-row v-if="comment.deleted === 1">
<el-row style="margin-top: 15px">
<p class="comment-comment">{{comment.comment}}</p>
</el-row>
<el-row class="fa-pull-right pb-1">
<p class="comment-user-time ">{{formatDate(comment.comment_time)}}</p>
</el-row>
</el-row>
<el-row style="margin-left: 30px">
<div v-for="reply in comment.replies">
<ReplyComponent :reply="reply" :forum="forum"/>
</div>
</el-row>
</el-card>
</div>
</template>
<script>
import moment from "moment";
import ReplyComponent from "./ReplyComponent";
export default {
name: "Comment",
props: {
comment: Object,
forum: Object,
},
components: {
ReplyComponent
},
data() {
return {
canEdit: Boolean,
interval: null,
mode: '',
form: {
comment: '',
},
};
},
methods: {
openReply(row) {
this.dialogReplyVisible = true;
this.parent = row;
},
editComment(item) {
this.mode = 'Editar';
this.form = _.cloneDeep(item);
this.dialogFormVisible = true;
},
checkTime() {
var minutes = moment().diff(moment(this.comment.comment_time), 'minutes');
if (minutes >= 30) {
this.canEdit = false;
// here you could also already clear the interval, since it won't change
} else if (minutes <= 29) {
this.canEdit = true;
}
}
},
created() {
this.checkTime();
this.interval = setInterval(() => {
this.checkTime();
}, 10000);
},
beforeDestroy() {
clearInterval(this.interval);
}
};
</script>
If I understood your questions correctly, you want to communicate (pass data) from Child components to parent component (page).
The way we can achieve this is by $emitting events from Child components and have the parent component respond to these events.
So, in your editComment and openReply methods you would fire events like this:
openReply(row) {
this.$emit('openReply', {any payload that you would want pass to parent});
},
editComment(item) {
this.$emit('editComment', {any payload that you would want pass to parent}
},
And, in your parent component / page you would subscribe and handle those events. Pseudo-code below:
<Comment v-on:openReply="handleOpenReply"
v-on:editCommnet="handleEditComment"/>
Further Reading:
Passing Data to Child Components With Props
Listening to Child Components Events
In the HeaderComponent method 'clickPrices' is called on click
<template>
<header>
<div class="d-flex flex-column flex-md-row align-items-center p-3 px-md-4 mb-3 bg-white border-bottom shadow-sm">
<h5 class="my-0 mr-md-auto font-weight-normal">Company name</h5>
<nav class="my-2 my-md-0 mr-md-3">
<a class="p-2 text-dark" href="#">Features</a>
<a class="p-2 text-dark">Enterprise</a>
<a class="p-2 text-dark" #click="clickPrices()">Get pricing</a>
</nav>
<a class="btn btn-outline-primary " href="#">Sign up</a>
</div>
</header>
</template>
<script>
export default {
name: "HeaderComponent",
methods: {
clickPrices() {
...
},
},
}
</script>
<style scoped>
</style>
and there is a Pricing Component in which I make a request to the server in the method 'getPricing'
<template>
<div class="wrap-pricing">
<div class="pricing-header px-3 py-3 pt-md-5 pb-md-4 mx-auto text-center">
<h1 class="display-4">Pricing</h1>
<p class="lead">Quickly build an effective pricing table for your potential customers with this Bootstrap example. It’s built with default Bootstrap components and utilities with little customization.</p>
</div>
<div class="container">
<div class="card-deck mb-3 text-center">
<div class="card mb-4 shadow-sm">
<div class="card-header">
<h4 class="my-0 font-weight-normal">Lorem</h4>
</div>
<div class="card-body">
<h1 class="card-title pricing-card-title">$10 <small class="text-muted">/ mo</small></h1>
<ul class="list-unstyled mt-3 mb-4">
<li>Lorem</li>
<li>Lorem</li>
<li>Lorem</li>
<li>Lorem</li>
</ul>
<button type="button" class="btn btn-lg btn-block"></button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import router from '../routes.js';
import { axios } from 'axios';
export default {
name: "PriceComponent",
methods: {
getPricing() {
axios.get('api/pricing').then((response) => {
//some actions
router.push('prices');
});
},
},
}
</script>
<style scoped>
</style>
How should I process the result of the 'сlickPrices' HeaderComponent method?
Or am I waiting for your ways, how can I get data in another by clicking in one component
You can emit an event and let the parent component handle the fetching and pass the data as props to the child component.
Or another way is to directly listen to the event as follows
Component 1:
this.$root.$emit('clickPrices', data);
Component 2:
mounted() {
this.$root.$on('clickPrices', data => {
this.getPricing();
});
}
Check out the answer by #alex
Since you are using 2 independent components (one is not included in the other), you cannot pass props
Things you can do -
Instead of fetching all the prices on click, just create a created hook like so, which will fetch all the pricing details whenever your component is created -
created () {
this.getPricing()
},
methods: {
getPricing() {
axios.get('api/pricing').then((response) => {
//some actions
router.push('prices');
});
},
},
Use State and when a user clicks on the button, pricing details are fetched and added in the state. And you can just use the state anywhere in your application like so -
this.$store.state.prices
Let me know if it works, if not we will find some other solution for you!
Is there a way to only display a slot if it has any content?
For example, I'm building a simple Card.vue component, and I only want the footer displayed if the footer slot has content:
Template
<template>
<div class="panel" :class="panelType">
<div class="panel-heading">
<h3 class="panel-title">
<slot name="title">
Default Title
</slot>
</h3>
</div>
<div class="panel-body">
<slot name="body"></slot>
<p class="category">
<slot name="category"></slot>
</p>
</div>
<div class="panel-footer" v-if="hasFooterSlot">
<slot name="footer"></slot>
</div>
</div>
</template>
Script
<script>
export default {
props: {
active: true,
type: {
type: String,
default: 'default',
},
},
computed: {
panelType() {
return `panel-${this.type}`;
},
hasFooterSlot() {
return this.$slots['footer']
}
}
}
</script>
In in View:
<card type="success"></card>
Since the above component doesn't contain a footer, it should not be rendered, but it is.
I've tried using this.$slots['footer'], but this returns undefined.
Does anyone have any tips?
It should be available at
this.$slots.footer
So, this should work.
hasFooterSlot() {
return !!this.$slots.footer;
}
Example.
You should check vm.$slots and also vm.$scopedSlots for it.
hasSlot (name = 'default') {
return !!this.$slots[ name ] || !!this.$scopedSlots[ name ];
}
CSS simplifies this a lot. Just use the following code and voila!
.panel-footer:empty {
display: none;
}
This is the solution for Vue 3 composition API:
<template>
<div class="md:grid md:grid-cols-5 md:gap-6">
<!-- Here, you hide the wrapper if there is no used slot or empty -->
<div class="md:col-span-2" v-if="hasTitle">
<slot name="title"></slot>
</div>
<div class="mt-5 md:mt-0"
:class="{'md:col-span-3': hasTitle, 'md:col-span-5': !hasTitle}">
<div class="bg-white rounded-md shadow">
<div class="py-7">
<slot></slot>
</div>
</div>
</div>
</div>
</template>
<script>
import {ref} from "vue";
export default {
setup(props, {slots}) {
const hasTitle = ref(false)
// Check if the slot exists by name and has content.
// It returns an empty array if it's empty.
if (slots.title && slots.title().length) {
hasTitle.value = true
}
return {
hasTitle
}
}
}
</script>
Now, in Vue3 composition API , you can use useSlots.
<script setup>
import { useSlots } from 'vue'
const slots = useSlots()
</script>
<template>
<div v-if="slots.content" class="classname">
<slot name="content"></slot>
</div>
</template>
In short do this in inline:
<template lang="pug">
div
h2(v-if="$slots.title")
slot(name="title")
h3(v-if="$slots['sub-title']")
slot(name="sub-title")
</template>
I have ran into a similiar issue but across a wide code base and when creating atomic design structured components it can be tiring writing hasSlot() methods all the time and when it comes to TDD - its one more method to test... Saying that, you can always put the raw logic in a v-if but i have found that the template end up cluttered and harder to read on occasions especially for a new dev checking out the code structure.
I was tasked to find out a way of removing parent divs of slots when the slot isnt provided.
Issue:
<template>
<div>
<div class="hello">
<slot name="foo" />
</div>
<div class="world">
<slot name="bar" />
</div>
</div>
</template>
//instantiation
<my-component>
<span slot="foo">show me</span>
</my-component>
//renders
<div>
<div class="hello">
<span slot="foo">show me</span>
</div>
<div class="world"></div>
</div>
as you can see, the issue is that i have an almost 'trailing' div, that could provide styling issues when the component author decides there is no need for a bar slot.
ofcourse we could go <div v-if="$slots.bar">...</div> or <div v-if="hasBar()">...</div> etc but like i said - that can get tiresome and eventually end up harder to read.
Solution
My solution was to make a generic slot component that just rendered out a slot with a surrounding div...see below.
//slot component
<template>
<div v-if="!!$slots.default">
<slot />
</div>
</template>
//usage within <my-component/>
<template>
<div>
<slot-component class="hello">
<slot name="foo"/>
</slot-component>
<slot-component class="world">
<slot name="bar"/>
</slot-component>
</div>
</template>
//instantiation
<my-component>
<span slot="foo">show me</span>
</my-component>
//renders
<div>
<div class="hello">
<span>show me</span>
</div>
</div>
I came into use-case issues when trying this idea and sometimes it was my markup structure that needed to change for the benefit of this approach.
This approach reduces the need for small slot checks within each component template. i suppose you could see the component as a <conditional-div /> component...
It is also worth noting that applying attributes to the slot-component instantiation (<slot-component class="myClass" data-random="randomshjhsa" />) is fine as the attributes trickle into the containing div of the slot-component template.
Hope this helps.
UPDATE
I wrote a plugin for this so the need for importing the custom-slot component in each consumer component is not needed anymore and you will only have to write Vue.use(SlotPlugin) in your main.js instantiation. (see below)
const SLOT_COMPONENT = {
name: 'custom-slot',
template: `
<div v-if="$slots.default">
<slot />
</div>
`
}
const SLOT_PLUGIN = {
install (Vue) {
Vue.component(SLOT_COMPONENT.name, SLOT_COMPONENT)
}
}
export default SLOT_PLUGIN
//main.js
import SlotPlugin from 'path/to/plugin'
Vue.use(SlotPlugin)
//...rest of code
Initially I thought https://stackoverflow.com/a/50096300/752916 was working, but I had to expand on it a bit since $scopeSlots returns a function which is always truthy regardless of its return value. This is my solution, though I've come to the conclusion that the real answer to this question is "doing this is an antipattern and you should avoid it if possible". E.g. just make a separate footer component that could be slotted in.
Hacky solution
hasFooterSlot() {
const ss = this.$scopedSlots;
const footerNodes = ss && ss.footer && ss.footer();
return footerNodes && footerNodes.length;
}
Best Practice (helper component for footer)
const panelComponent = {
template: `
<div class="nice-panel">
<div class="nice-panel-content">
<!-- Slot for main content -->
<slot />
</div>
<!-- Slot for optional footer -->
<slot name="footer"></slot>
</div>
`
}
const footerComponent = {
template: `
<div class="nice-panel-footer">
<slot />
</div>
`
}
var app = new Vue({
el: '#app',
components: {
panelComponent,
footerComponent
},
data() {
return {
name: 'Vue'
}
}
})
.nice-panel {
max-width: 200px;
border: 1px solid lightgray;
}
.nice-panel-content {
padding: 30px;
}
.nice-panel-footer {
background-color: lightgray;
padding: 5px 30px;
text-align: center;
}
<script src="https://unpkg.com/vue#2.6.11/dist/vue.min.js"></script>
<div id="app">
<h1>Panel with footer</h1>
<panel-component>
lorem ipsum
<template #footer>
<footer-component> Some Footer Content</footer-component>
</template>
</panel-component>
<h1>Panel without footer</h1>
<panel-component>
lorem ipsum
</panel-component>
</div>
Hope I understand this right. Why not using a <template> tag, which is not rendered, if the slot is empty.
<slot name="foo"></slot>
Use it like this:
<template slot="foo">
...
</template>
For Vue 3:
Create an utility function
//utils.js
function isSlotHasContent(slotName, slots) {
return Boolean(!!slots[slotName] && slots[slotName]()[0].children.length > 0);
}
In your component:
<script setup>
import { isSlotHasContent } from 'path/to/utils.js';
const slots = useSlots();
// "computed" props has a better performance
const isFooSlotHasContent = computed(() => isSlotHasContent('foo', slots));
</script>
<template>
<div>
<div v-if="isFooSlotHasContent">
<slot name="foo" />
</div>
<div v-if="!isFooSlotHasContent">
Some placeholder
</div>
</div>
</template>
TESTED
So this work for me in vue 3:
I use onMounted to first get the value, and then onUpdate so the value can update.
<template>
<div v-if="content" class="w-1/2">
<slot name="content"></slot>
</div>
</template>
<script>
import { ref, onMounted, defineComponent, onUpdated } from "vue";
export default defineComponent({
setup(props, { slots }) {
const content = ref()
onMounted(() => {
if (slots.content && slots.content().length) {
content.value = true
}
})
onUpdated(() => {
content.value = slots.content().length
console.log('CHECK VALUE', content.value)
})
})
</script>
#Bert answer does not seem to work for dynamic templates like <template v-slot:foo="{data}"> ... </template>.
i ended up using:
return (
Boolean(this.$slots.foo) ||
Boolean(typeof this.$scopedSlots.foo == 'function')
);
I like the Solution of #AlexMA however in my case I needed to pass props to the function in order to get the nodes to show up.
Here is an example of how I am passing the "row" to the scoped slot, in my case the row contains a type param that I want to test against in the calling component.
<other-component>
<template v-slot:expand="{ row }" v-if="!survey.editable">
<div v-if="row.type != 1" class="flex">
{{ row }}
</div>
</template>
</other-component>
In "other-component" I have the template defined as
<template>
<div>
<div v-for="(row, index) in rows">
{{ hasSlotContent(row) }}
<slot name="expand" :row="row"> </slot>
</div>
</div>
</template>
Because the v-slot requires "row" to be passed to it I created a a method
methods:{
hasSlotContent(row){
const ss = this.$scopedSlots
const nodes = ss && ss.expand && ss.expand({ row: row })
return !!(nodes && nodes.length)
}
}
I call this on each iteration so that it can evaluate itself and give back the appropriate response.
you can use the "hasSlotContent(row)" method where-ever you need it, in my example I'm just outputting the truthy value to the DOM.
I hope this helps someone come to a quicker solution.
Reposting a Vue 3 solution from Github, which also works with Options API, since there was a fairly upvoted method from an Issue there:
The comment itself: https://github.com/vuejs/core/issues/4733#issuecomment-1024816095
The function (remove types if you're not writing TypeScript):
import {
Comment,
Text,
Slot,
VNode,
} from 'vue';
export function hasSlotContent(slot: Slot|undefined, slotProps = {}): boolean {
if (!slot) return false;
return slot(slotProps).some((vnode: VNode) => {
if (vnode.type === Comment) return false;
if (Array.isArray(vnode.children) && !vnode.children.length) return false;
return (
vnode.type !== Text
|| (typeof vnode.children === 'string' && vnode.children.trim() !== '')
);
});
}
This works just as fine, if you delete the slotProps argument (unless you need it).