Add Close Button to Vue with Popper.js Modal Component - vue.js

UPDATED Here is a codesandbox recreating the issue - Modal Does not hide: https://codesandbox.io/s/vue3-popperjs-modal-64762?file=/src/components/Modal.vue
I am new to Vue and cannot figure out how to Close the modal from the Hide Modal button within the component. I am using Vue.js and Popper.js for a modal/dropdown component.
I have tried emitting, rels and passing a prop but cannot get it to function correctly. I stripped it down to the shell code below.
** Main Component **
<template>
<div>
<Head title="Main" />
<div class="flex items-center">
<div class="flex">
<dropdown :auto-close="false" class="focus:z-10 px-4 hover:bg-gray-100 focus:border-white rounded-l focus:ring md:px-6" placement="auto">
<template #default>
<div class="flex">
<button id="open" class="btn-indigo" type="button">Open Modal</button>
</div>
</template>
<template #dropdown>
<div class="mt-2 px-4 py-6 w-screen bg-white rounded shadow-xl" style="maxWidth: 600px">
<h1>Modal Content</h1>
<button id="close" class="btn-indigo" type="button">Hide Modal</button>
</div>
</template>
</dropdown>
</div>
</div>
</div>
</template>
<script>
import { Head } from '#inertiajs/inertia-vue3'
import Dropdown from '#/Shared/Dropdown'
export default {
components: {
Head,
Dropdown,
},
}
</script>
** And Dropdown Component **
<template>
<button type="button" #click="show = true">
<slot />
<teleport v-if="show" to="#dropdown">
<div>
<div style="position: fixed; top: 0; right: 0; left: 0; bottom: 0; z-index: 99998; background: black; opacity: 0.2" #click="show = false" />
<div ref="dropdown" style="position: absolute; z-index: 99999" #click.stop="show = !autoClose">
<slot name="dropdown" />
</div>
</div>
</teleport>
</button>
</template>
<script>
import { createPopper } from '#popperjs/core'
export default {
props: {
placement: {
type: String,
default: 'bottom-end',
},
autoClose: {
type: Boolean,
default: true,
},
},
data() {
return {
show: false,
}
},
watch: {
show(show) {
if (show) {
this.$nextTick(() => {
this.popper = createPopper(this.$el, this.$refs.dropdown, {
placement: this.placement,
modifiers: [
{
name: 'preventOverflow',
options: {
altBoundary: true,
},
},
],
})
})
} else if (this.popper) {
setTimeout(() => this.popper.destroy(), 100)
}
},
},
mounted() {
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
this.show = false
}
})
},
}
</script>

Related

Conditional CSS proprety set in computed object doesn't evaluate on the browser

I am trying to make the header element's height size to change depending on a criteria. I set the desired heights in a headerHeightClass inside a computed object but it doesn't seem to work.
<template>
<header :class="['w-full', 'text-sm', 'headerHeightClass']">
<div class="fixed top-0 left-0 w-full h-16 bg-white">
<!--Some irrelevant html code -->
</div>
</header>
</template>
<script>
export default {
name: "MainNav",
components: {},
data() {
return {
isLoggedIn: false,
};
},
computed: {
headerHeightClass() {
return {
"h-16": !this.isLoggedIn,
"h-32": this.isLoggedIn,
};
},
},
methods: {
loginUser() {
this.isLoggedIn = true;
},
},
};
</script>
I also tried:
computed: {
headerHeightClass() {
return this.isLoggedIn? "h-32": "h-16",
},
},
The headerHeightClass appears as a simple string in the browser.
You need to write it like this instead of using headerHeightClass as a string (remove ' ')
<template>
<header :class="['w-full', 'text-sm', headerHeightClass]">
<div class="fixed top-0 left-0 w-full h-16 bg-white">
<!--Some irrelevant html code -->
</div>
</header>
</template>
This can also be written without using computed
<template>
<header :class="[
'w-full text-sm',
{
'h-16':!isLoggedIn,
'h-32':isLoggedIn,
}
]">
<div class="fixed top-0 left-0 w-full h-16 bg-white">
<!--Some irrelevant html code -->
</div>
</header>
</template>
You can give a try like this (Just for a demo purpose I used Vue 2 version) :
Vue.component('headerComponent', {
props: ['msg'],
template: '<p>{{ msg }}</p>'
});
var app = new Vue({
el: '#app',
data: {
isLoggedIn: true
}
});
.w-full {
background-color: gray;
width: 100%;
}
.text-sm {
font-size: 12px;
}
.h-16 {
height: 16px;
}
.h-32 {
height: 32px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<header-component
msg="This is a header component"
:class="['w-full text-sm', { 'h-16':!isLoggedIn, 'h-32':isLoggedIn }]">
</header-component>
</div>

Vue don't give #click to a single element when using v-for to create them

I'm trying to make every single element created with the v-for the ability to toggle between 'face' class, but it happens that it toggles everything created with the v-for.
<div class="deckPairs">
<div class="deckBoxOne" v-for="base in cards" :key="base.id" >
<div class="deckBoxBlanc" :class="{face : face}" #click="face = !face">
<img class="deckPairsImage" style="z-index: 3" alt="Vue logo" :src="require('../assets/rightCardSide.png')">
</div>
<div class="deckBoxImg" :class="{face : !face}" #click="face = !face">
<img class="deckPairsImages" style="z-index: 2" :src="require(`../assets/images/${base.url}`)">
</div>
</div>
</div>
Script:
export default {
setup() {
const face = ref(false)
return { face }
},
data(){
return {
face: false,
}
},
methods: {
}
}
Create a component to encapsulate the v-for content.
// new-component.vue
<template>
<div>
<div class="deckBoxBlanc" :class="{face : face}" #click="face = !face">
<img class="deckPairsImage" style="z-index: 3" alt="Vue logo" :src="require('../assets/rightCardSide.png')">
</div>
<div class="deckBoxImg" :class="{face : !face}" #click="face = !face">
<img class="deckPairsImages" style="z-index: 2" :src="require(`../assets/images/${base.url}`)">
</div>
</div>
</template>
export default {
data(){
return {
face: false
}
}
}
Now update v-for to use the new component.
<div class="deckPairs">
<div class="deckBoxOne" v-for="base in cards" >
<new-component :key="base.id"/>
</div>
</div>
You can use base.id instead boolean and v-show directive:
const { ref } = Vue
const app = Vue.createApp({
el: "#demo",
setup() {
const face = ref(false)
const cards = ref([{id: 1, url: "https://picsum.photos/100"}, {id: 2, url: "https://picsum.photos/101"}, {id: 3, url: "https://picsum.photos/102"}])
const rightCardSide = ref("https://picsum.photos/103")
const setFace = (val) => {
face.value = val
}
return { cards, face, rightCardSide, setFace }
},
})
app.mount('#demo')
.deckPairs {
display: flex;
}
.deckBoxOne {
cursor: pointer;
}
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
<div id="demo">
<div class="deckPairs">
<div class="deckBoxOne" v-for="base in cards" :key="base.id" >
<div class="deckBoxBlanc" v-show=" face !== base.id" #click="setFace(base.id)">
<img class="deckPairsImage" alt="Vue logo" :src="rightCardSide">
</div>
<div class="deckBoxImg" v-show="face === base.id" #click="setFace(false)">
<img class="deckPairsImages" :src="base.url">
</div>
</div>
</div>
</div>

VueJs | How to combine a In and out animation with library Animate.css?

I'm starting with VueJs.
I would like to combine animation in and out in a modal.
I think I should do a function but can't find how to.
Here my code:
<template>
<div class="backgroundImage" :style="{'background-image': 'url(' + require('../../assets/bkg.jpg') + ')'}">
<div v-if="showModal" #click="showModal = false"></div>
<div class="modal" v-if="showModal">
<div class="animate__animated animate__bounceIn animate__slow">
<img id="synthesisFt" src="../../assets/popup-ftt.jpg" alt="Logo FunkTheTown" title="FunkTheTown" #click="showModal = false"/>
</div>
</div>
<div>
<img class="logo-img" src="../../assets/logo.png" alt="Logo FunkTheTown" title="FunkTheTown" />
</div>
</div>
</template>
<script>
export default {
data () {
return {
showModal: false
}
},
mounted:function(){
this.popup()
},
methods: {
popup : function () {
setTimeout(() => {
this.showModal = true;
}, 3000);
},
// classChange : function () {
// showmodal = false;
// }
}
}
for the off I would like to use animate__animated animate__bounceOut when I click on the modal.
I've resolved the issue, here the code :
<template>
<div class="backgroundImage" :style="{'background-image': 'url(' + require('../../assets/bkg.jpg') + ')'}">
<div v-if="showModal" #click="showModal = false"></div>
<div class="modal" v-if="showModal">
<div class="animate__animated animate__bounceIn animate__slow">
<img id="synthesisFt" src="../../assets/popup-ftt.jpg" alt="Logo FunkTheTown" title="FunkTheTown" #click="anim2()"/>
</div>
</div>
<div class="animate__animated animate__backInDown animate__slow" v-if="showModalLogin">
<div class="modalLogin">
<label>Login</label>
<input class="loginInput" type="text" placeholder="email...">
<label>Password</label>
<input class="loginPassword" type="password" placeholder="password...">
<button class="connexion">Connexion</button>
<button class="join">Join</button>
</div>
</div>
<div>
<img class="logo-img" src="../../assets/logo.png" alt="Logo FunkTheTown" title="FunkTheTown" />
</div>
<div class="animate__animated animate__rotateIn" v-if="showModalLogo" >
<img class="full_logo" src="../../assets/F2T_logo_color.png" >
</div>
<div class="animate__animated animate__rotateIn" v-if="showModalLogo">
<img class="full_logo_right" src="../../assets/F2T_logo_coloreverse.png" >
</div>
</div>
</template>
<script>
export default {
data () {
return {
showModal: false,
showModalLogin: false,
showModalLogo:false,
}
},
mounted:function(){
this.popup()
},
methods: {
popup : function () {
setTimeout(() => {
this.showModal = true;
}, 3000);
},
anim2 : function () {
this.showModal= false;
this.showModalLogin= true;
this.showModalLogo =true;
const element = document.querySelector('.logo-img');
element.classList.add('animate__animated','animate__bounceOut');
}
// classChange : function () {
// showmodal = false;
// }
}
}

How to Add Vue Component as an HTML Tag

I'm new to using VueJS, and I'm working on a learning project right now.
I have a component called "Draggable", and another one called "ModalPage".
"ModalPage" is as follows:
The code for this page is below:
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<button #click="propagate">Propagate</button>
<h3>Vue Modals</h3>
<ul id="list">
</ul>
<!-- <div v-html="template"></div> -->
</div>
</template>
<script>
import draggable from '#/components/Draggable.vue';
export default {
name: 'ModelPage',
components: {
draggable,
},
data () {
return {
msg: 'Welcome to the Modal Popup Page',
template: `<draggable></draggale>`,
}
},
methods: {
propagate () {
// console.log("propagated")
list.append(`<draggable></draggale>`)
}
}
}
</script>
I also have a component called "Draggable" and its code is as follows:
<template>
<div id="app">
<VueDragResize :isActive="true" :w="200" :h="200" v-on:resizing="resize" v-on:dragging="resize">
<h3>Hello World!</h3>
<p>{{ top }} х {{ left }} </p>
<p>{{ width }} х {{ height }}</p>
</VueDragResize>
</div>
</template>
<script>
import VueDragResize from 'vue-drag-resize';
export default {
name: 'app',
components: {
VueDragResize
},
data() {
return {
width: 0,
height: 0,
top: 0,
left: 0
}
},
methods: {
resize(newRect) {
this.width = newRect.width;
this.height = newRect.height;
this.top = newRect.top;
this.left = newRect.left;
}
}
}
</script>
What i want to do is to be able to click the "Propagate" button on the page, and have a <draggable></draggable> html element appended to the page, as follows:
<ul id="list">
<draggable></draggable>
</ul>
I'm completely stumped with this. Can anyone help me please?
v-if will do your job
Updated
You can use v-for as discussion:
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<button #click="propagate">Propagate</button>
<h3>Vue Modals</h3>
<ul id="list">
<draggable v-for="index in count" :key="index>
</draggable>
</ul>
<!-- <div v-html="template"></div> -->
</div>
</template>
<script>
import draggable from '#/components/Draggable.vue';
export default {
name: 'ModelPage',
components: {
draggable,
},
data () {
return {
msg: 'Welcome to the Modal Popup Page',
template: `<draggable></draggale>`,
count: 0
}
},
methods: {
propagate () {
// console.log("propagated")
this.count++
}
}
}
</script>

Is it possible to sync Vuejs components displayed multiple times on the same page?

I have a web page that displays items. For each items there is a button (vuejs component) which allow user to toggle (add/remove) this item to his collection.
Here is the component:
<template lang="html">
<button type="button" #click="toggle" name="button" class="btn" :class="{'btn-danger': dAdded, 'btn-primary': !dAdded}">{{ dText }}</button>
</template>
<script>
export default {
props: {
added: Boolean,
text: String,
id: Number,
},
data() {
return {
dAdded: this.added,
dText: this.text,
dId: this.id
}
},
watch: {
added: function(newVal, oldVal) { // watch it
this.dAdded = this.added
},
text: function(newVal, oldVal) { // watch it
this.dText = this.text
},
id: function(newVal, oldVal) { // watch it
this.dId = this.id
}
},
methods: {
toggle: function(event) {
axios.post(route('frontend.user.profile.pop.toggle', {
pop_id: this.dId
}))
.then(response => {
this.dText = response.data.message
let success = response.data.success
this.dText = response.data.new_text
if (success) {
this.dAdded = success.attached.length
let cardPop = document.getElementById('card-pop-'+this.dId);
if(cardPop)
cardPop.classList.toggle('owned')
}
})
.catch(e => {
console.log(e)
})
}
}
}
</script>
For each item, the user can also open a modal, loaded by a click on this link:
<a href="#" data-toggle="modal" data-target="#popModal" #click="id = {{$pop->id}}">
<figure>
<img class="card-img-top" src="{{ URL::asset($pop->img_path) }}" alt="Card image cap">
</figure>
</a>
The modal is also a Vuejs component:
<template>
<section id="pop" class="h-100">
<div class="card">
<div class="container-fluid">
<div class="row">
<div class="col-12 col-lg-1 flex-column others d-none d-xl-block">
<div class="row flex-column h-100">
<div v-for="other_pop in pop.other_pops" class="col">
<a :href="route('frontend.pop.collection.detail', {collection: pop.collection.slug, pop: other_pop.slug})">
<img :src="other_pop.img_path" :alt="'{{ other_pop.name }}'" class="img-fluid">
</a>
</div>
<div class="col active order-3">
<img :src="pop.img_path" :alt="pop.name" class="img-fluid">
</div>
</div>
</div>
<div class="col-12 col-lg-6 content text-center">
<div class="row">
<div class="col-12">
<img :src="pop.img_path" :alt="pop.name" class="img-fluid">
</div>
<div class="col-6 text-right">
<toggle-pop :id="pop.id" :added="pop.in_user_collection" :text="pop.in_user_collection ? 'Supprimer' : 'Ajouter'"></toggle-pop>
</div>
<div class="col-6 text-left">
<!-- <btnaddpopwhishlist :pop_id="propid" :added="pop.in_user_whishlist" :text="pop.in_user_whishlist ? 'Supprimer' : 'Ajouter'"></btnaddpopwhishlist> -->
</div>
</div>
</div>
<div class="col-12 col-lg-5 infos">
<div class="header">
<h1 class="h-100">{{ pop.name }}</h1>
</div>
<div class="card yellow">
<div class="card p-0">
<div class="container-fluid">
<div class="row">
<div class="col-3 py-2">
</div>
<div class="col-6 py-2 bg-lightgray">
<h4>Collection:</h4>
<h3>{{ pop.collection ? pop.collection.name : '' }}</h3>
</div>
<div class="col-3 py-2 bg-lightgray text-center">
<a :href="route('frontend.index') + 'collections/' + pop.collection.slug" class="btn-round right white"></a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</template>
<script>
export default {
props: {
id: Number
},
data() {
return {
pop: {
collection: {
}
}
}
},
ready: function() {
if (this.propid != -1)
this.fetchData()
},
watch: {
id: function(newVal, oldVal) { // watch it
// console.log('Prop changed: ', newVal, ' | was: ', oldVal)
this.fetchData()
}
},
computed: {
imgSrc: function() {
if (this.pop.img_path)
return 'storage/images/pops/' + this.pop.img_path
else
return ''
}
},
methods: {
fetchData() {
axios.get(route('frontend.api.v1.pops.show', this.id))
.then(response => {
// JSON responses are automatically parsed.
// console.log(response.data.data.collection)
this.pop = response.data.data
})
.catch(e => {
this.errors.push(e)
})
// console.log('fetchData')
}
}
}
</script>
Here is my app.js script :
window.Vue = require('vue');
Vue.component('pop-modal', require('./components/PopModal.vue'));
Vue.component('toggle-pop', require('./components/TogglePop.vue'));
const app = new Vue({
el: '#app',
props: {
id: Number
}
});
I would like to sync the states of the component named toggle-pop, how can I achieve this ? One is rendered by Blade template (laravel) and the other one by pop-modal component. But they are just the same, displayed at different places.
Thanks.
You could pass a state object as a property to the toggle-pop components. They could use this property to store/modify their state. In this way you can have multiple sets of components sharing state.
Your component could become:
<template lang="html">
<button type="button" #click="toggle" name="button" class="btn" :class="{'btn-danger': sstate.added, 'btn-primary': !sstate.added}">{{ sstate.text }}</button>
</template>
<script>
export default {
props: {
sstate: {
type: Object,
default: function() {
return { added: false, text: "", id: -1 };
}
}
},
data() {
return {};
},
methods: {
toggle: function(event) {
axios.post(route('frontend.user.profile.pop.toggle', {
pop_id: this.sstate.id
}))
.then(response => {
this.sstate.text = response.data.message
let success = response.data.success
this.sstate.text = response.data.new_text
if (success) {
this.sstate.ddded = success.attached.length
let cardPop = document.getElementById('card-pop-'+this.sstate.id);
if(cardPop)
cardPop.classList.toggle('owned')
}
})
.catch(e => {
console.log(e)
})
}
};
</script>
Live demo
https://codesandbox.io/s/vq8r33o1w7
If you are 100% sure that all toggle-pop components should always have the same state, you can choose to not define data as a function. Just declare it as an object.
data: {
dAdded: this.added,
dText: this.text,
dId: this.id
}
In https://v2.vuejs.org/v2/guide/components.html#data-Must-Be-a-Function, it mentions
a component’s data option must be a function, so that each instance
can maintain an independent copy of the returned data object
If Vue didn’t have this rule, clicking on one button would affect the
data of all other instances
Since you want to sync the data of all toggle-pop component instances, you don't have to follow the data option must be a function rule.