I wrote a dialog component (global) to show modal dialogs with overlays like popup forms.
Right now the dialog gets rendered inside the component where it is used. This leads to overlapping content, if there is something with position relative in the html code afterwards.
I want it to be rendered in the root App component at the very end so I can force the dialog to be always ontop of every other content.
This is my not working solution:
I tried to use named slots, hoping, that they work backwards in the component tree too. Unfortunately they don't seem to do that.
Anybody a solution how to do it?
My next idea would be to render with an extra component that is stored in the app component and register the dialogs in the global state. But that solution would be super complicated and looks kinda dirty.
The dialog component:
<template v-slot:dialogs>
<div class="dialog" :class="{'dialog--open': show, 'dialog--fullscreen': fullscreen }">
<transition name="dialogfade" duration="300">
<div class="dialog__overlay" v-if="show && !fullscreen" :key="'overlay'" #click="close"></div>
</transition>
<transition name="dialogzoom" duration="300">
<div class="dialog__content" :style="{'max-width': maxWidth}" v-if="show" :key="'content'">
<slot></slot>
</div>
</transition>
</div>
</template>
<script>
export default {
name: "MyDialog",
props: {show: {
type: Boolean,
default: false
},
persistent: {
type: Boolean,
default: true
},
fullscreen: {
type: Boolean,
default: false
},
maxWidth: {
type: String,
default: '600px'
}
},
data: () => ({}),
methods: {
close() {
if(!this.persistent) {
this.$emit('close')
}
}
}
}
</script>
The template of the app component:
<template>
<div class="application">
<div class="background">
<div class="satellite"></div>
<div class="car car-lr" :style="{ transform: `translateY(${car.x}px)`, left: adjustedLRLeft + '%' }" v-for="car in carsLR"></div>
</div>
<div class="content">
<login v-if="!$store.state.user"/>
<template v-else>
<main-menu :show-menu="showMainMenu" #close="showMainMenu = false"/>
<router-view/>
</template>
<notifications/>
<div class="dialogs"><slot name="dialogs"></slot></div>
</div>
</div>
</template>
Another possibility is to use portals. These provide a way to move any element to any place in the dom. Checkout the following library: https://github.com/LinusBorg/portal-vue
You can just place the dialog component directly in the app component and handle the dialog logic/which dialog to display in that component?
In case you want to trigger these dialogs from other places in your app, this would would be a good use case for vuex! That, combined with dynamic webpack imports is how I handle this.
With the help of the guys from the vuetify2 project, I found the solution. The dialog component gets an ref="dialogContent" attribute and the magic happens inside the beforeMount function.
<template>
<div class="dialog" ref="dialogContent" :class="{'dialog--open': show, 'dialog--fullscreen': fullscreen }">
<transition name="dialogfade" duration="300">
<div class="dialog__overlay" v-if="show && !fullscreen" :key="'overlay'" #click="close"></div>
</transition>
<transition name="dialogzoom" duration="300">
<div class="dialog__content" :style="{'max-width': maxWidth}" v-if="show" :key="'content'">
<slot></slot>
</div>
</transition>
</div>
</template>
<script>
export default {
name: "MyDialog",
props: {
show: {
type: Boolean,
default: false
},
persistent: {
type: Boolean,
default: true
},
fullscreen: {
type: Boolean,
default: false
},
maxWidth: {
type: String,
default: '600px'
}
},
data: () => ({}),
methods: {
close() {
if (!this.persistent) {
this.$emit('close')
}
}
},
beforeMount() {
this.$nextTick(() => {
const target = document.getElementById('dialogs');
target.appendChild(
this.$refs.dialogContent
)
})
},
}
</script>
Related
I am trying to create a component for a popover using Bootstrap4 in Vue:
<template>
<div>
<div :id="uid + '_Content'" class="d-none">
<slot name="content">
</slot>
</div>
<div :class="'call' + uid">
<slot name="caller">
</slot>
</div>
</div>
</template>
<script>
module.exports = {
data() {
return {
uid: this.genUID()
}
},
props: {
title: {
type: String,
required: false,
default: ''
}
},
mounted() {
_this = this;
// PopOver Definition
const popover = new bootstrap.Popover(document.querySelector('.call' + this.uid), {
container: 'body',
title: this.title,
html: true,
placement: 'auto',
sanitize: false,
customClass: 'noselect',
content: () => {
return document.querySelector("#" + _this.uid + "_Content").innerHTML;
}
});
},
methods: {
genUID() {
return "Popover_" + Math.random().toString(16).slice(2);
}
}
}
</script>
However, when passing content to <slot name="content"> from another component, the data is not reactive. Is there any config information that I am missing to make it reactive? Is this even possible with Vue and (regular) Bootstrap (not Bootstrap-Vue).
You're losing reactivity because your content option to bootstrap.Popover is returning a string of your element's HTML, not the element itself. The popover just copies the HTML as it exists when it is opened. If you pass the element, Bootstrap will reparent the element itself into the popover, so changes to the element's children should be reflected. (Note that this could still be disrupted by a virtual DOM change that rewrote the element itself, which is why Bootstrap-Vue would still be better here.) If the popover might be reused, you'll need to reparent the element back into your component's own tree each time the popover is closed. You'll also need to make provision for the _Content element to only be hidden while it isn't reparented.
Here's how it all would look:
<template>
<div ref="container" class="Popover__Container">
<div ref="content" class="Popover__Content">
<slot name="content">
</slot>
</div>
<div ref="caller" class="Popover__Caller">
<slot name="caller">
</slot>
</div>
</div>
</template>
<script>
module.exports = {
props: {
title: {
type: String,
required: false,
default: ''
}
},
mounted() {
const content = this.$refs.content;
// PopOver Definition
const popover = new bootstrap.Popover(this.$refs.caller, {
container: 'body',
title: this.title,
html: true,
placement: 'auto',
sanitize: false,
customClass: 'noselect',
content: content
});
$(this.$refs.caller).on('hidden.bs.popover', () =>
{
this.$refs.container.prepend(content);
});
}
}
</script>
<style scoped>
.Popover__Container > .Popover__Content {
display: none;
}
</style>
(I've also replaced the UID approach with refs, since that is a more Vue-like approach.)
I have an vue component which uses Vue Draggable .
<template>
<div class="row my-5">
<div v-for="column in columns" :key="column.title" class="col">
<p class="font-weight-bold text-uppercase">{{column.title}}</p>
<!-- Draggable component comes from vuedraggable. It provides drag & drop functionality -->
<draggable :list="column.tasks" :animation="200" ghost-class="ghost-card" group="tasks" :move="checkMove">
<transition-group>
<task-card
v-for="(task) in column.tasks"
:key="task.id"
:task="task"
class="mt-3 cursor-move"
></task-card>
<!-- </transition-group> -->
</transition-group>
</draggable>
</div>
</div>
</template>
<script>
import draggable from "vuedraggable";
import TaskCard from "../board/TaskCard";
export default {
name: "App",
components: {
TaskCard,
draggable,
},
data() {
return {
columns: [
.....
],
};
},
methods: {
checkMove: function(evt){
console.log('moved');
}
},
};
</script>
In TaskCard Component -
<template>
<div class="bg-white shadow rounded p-3 border border-white">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2>{{task.id}}</h2>
<span>{{task.date}}</span>
</div>
<p class="font-weight-bold">{{task.title}}</p>
</div>
</template>
<script>
export default {
props: {
task: {
type: Object,
default: () => ({}),
},
},
};
</script>
When I move an item, I want a modal that confirms the change and only then move the item.
(ie. if I click on the cancel button inside the modal, the item should not be moved.)
How can this be achieved using the checkMove() function provided?
I don't think you can achieve this by using onMove event. The onEnd event it seems more suitable but unfortunately it doesn't have any cancel drop functionality.
So I think the only solution here is revert it back if the user decides to cancel.
You can listen on change event (See more in documentation)
<draggable
group="tasks"
v-model="column.tasks"
#change="handleChange($event, column.tasks)">
...
</draggable>
...
<button #click="revertChanges">Cancel</button>
<button #click="clearChanges">Yes</button>
And
...
handleChange(event, list) {
this.changes.push({ event, list })
this.modal = true
},
clearChanges() {
this.changes = []
this.modal = false
},
revertChanges() {
this.changes.forEach(({ event, list }) => {
if (event.added) {
let { newIndex } = event.added
list.splice(newIndex, 1)
}
if (event.removed) {
let { oldIndex, element } = event.removed
list.splice(oldIndex, 0, element)
}
if (event.moved) {
let { newIndex, oldIndex, element } = event.moved
list[newIndex] = list[oldIndex]
list[oldIndex] = element
}
})
this.changes = []
this.modal = false
}
...
JSFiddle example
I'm building simple modal component with Vue. I want to use this component in a parent components and to be able to toggle it from the parent.
This is my code now of the modal component:
<script>
export default {
name: 'Modal',
data() {
return {
modalOpen: true,
}
},
methods: {
modalToggle() {
this.modalOpen = !this.modalOpen
},
},
}
</script>
<template>
<div v-if="modalOpen" class="modal">
<div class="body">
body
</div>
<div class="btn_cancel" #click="modalToggle">
<i class="icon icon-cancel" />
</div>
</div>
</template>
I use the v-if to toggle the rendering and it works with the button i created inside my modal component.
However my problem is: I don't know how to toggle it with simple button from parent component. I don't know how to access the modalOpen data from the modal component
Ok, let's try to do it right. I propose to make a full-fledged component and control the opening and closing of a modal window using the v-model in parent components or in other includes.
1) We need declare prop - "value" in "props" for child component.
<script>
export default {
name: 'Modal',
props: ["value"],
data() {
return {
modalOpen: true,
}
},
methods: {
modalToggle() {
this.modalOpen = !this.modalOpen
},
},
}
</script>
2) Replace your "modalToggle" that:
modalToggle() {
this.$emit('input', !this.value);
}
3) In parent components or other includes declare "modal=false" var and use on component v-model="modal" and any control buttons for modal var.
summary
<template>
<div v-if="value" class="modal">
<div class="body">
body
</div>
<div class="btn_cancel" #click="modalToggle">
<i class="icon icon-cancel" />
</div>
</div>
</template>
<script>
export default {
name: "Modal",
props: ["value"],
methods: {
modalToggle() {
this.$emit("input", !this.value);
}
}
};
</script>
Example:
Vue.component('modal', {
template: '<div v-if="value" class="modal"><div class="body">modal body</div><div class="btn_cancel" #click="modalToggle">close modal<i class="icon icon-cancel" /></div></div>',
props: ["value"],
methods: {
modalToggle() {
this.$emit('input', !this.value);
}
}
});
// create a new Vue instance and mount it to our div element above with the id of app
var vm = new Vue({
el: '#app',
data:() =>({
modal: false
}),
methods: {
openModal() {
this.modal = !this.modal;
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div #click="openModal">Btn modal</div>
<modal v-model="modal"></modal>
</div>
With your current implementation I would suggest you to use refs
https://v2.vuejs.org/v2/guide/components-edge-cases.html#Accessing-Child-Component-Instances-amp-Child-Elements
So in your parent component add ref="child" to modal (child) component and then open your modal by calling this.$refs.child.modalToggle()
You can use an "activator" slot
You can use ref="xxx" on the child and access it from the parent
Looping out a number of boxes within the same component in vuejs.
Each box has a button that reveals more text using v-on:click.
The problem is that all the boxes respond to this at the same time.
Is there a way to target the specific button being clicked if there are several buttons in a component?
Is there some way to isolate each button so they all dont activate at once?
<div class="filter-list-area">
<button #click="show =!show"> {{text}} </button>
<ul class="filter-list-item-area">
<li class="filter-list-item " v-for="(items, key) in packages">
<div>
<img class="fit_rating">
</div>
<div class="filter-list-item-info" >
<h3>{{items.partner_university}}</h3>
<p> Match: {{items.match_value}}</p>
<div v-for="(courses, key) in courses">
<transition name="courses">
<div class="courses2" v-show="show">
<p v-if="courses.Pu_Id === items.pu_Id">
{{courses.Course_name}}
</p>
</div>
</transition>
</div>
</div>
</li>
</ul>
</div>
</template>
<script>
import testdata from '../App.vue'
export default {
data (){
return{
text: 'Show Courses',
testFilter: 'Sweden',
show: false
}
},
props: {
title: String,
likes: Number,
isPublished: Boolean,
commentIds: Array,
author: Object,
testuni: Array,
list: Array,
packages: Array,
courses: Array
},
methods:{
afunction(){
console.log(this.show);
},
changeText(){
if(this.show){
this.text = 'Hide Courses'
}
else{
this.text = "Show Courses"
}
}
},
mounted() {
this.afunction();
},
watch: {
show:
function() {
this.afunction()
this.changeText()
}
},
}
EDIT: I've created this before you posted the code example, but you could use same principle:
In your data add showMoreText, which will be used to track if show more data should be shown.
I would agree with #anaximander that you should use child components here
Simple example how to show/hide
<template>
<div>
<div v-for="(box, index) in [1,2,3,4,5]">
<div>
Box {{ box }} <button #click="toggleMore(index)">More</button>
</div>
<div v-show="showMoreText[index]">
More about box {{ box }}
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
showMoreText: {},
}
},
methods: {
toggleMore(index) {
/*
Adds a property to a reactive object, ensuring the new property is
also reactive, so triggers view updates.
https://vuejs.org/v2/api/#Vue-set
*/
this.$set(this.showMoreText, index, ! this.showMoreText[index])
}
}
}
</script>
This sounds like an ideal situation for a new child component, which will allow each instance of the new component to have its own separate state.
The child components can emit events to the parent, if cross-component communication is necessary.
I'm just starting out with VueJS and I was trying to port over a simple jQuery read more plugin I had.
I've got everything working except I don't know how to get access to the contents of the slot. What I would like to do is move some elements passed into the slot to right above the div.readmore__wrapper.
Can this be done simply in the template, or am I going to have to do it some other way?
Here's my component so far...
<template>
<div class="readmore">
<!-- SOME ELEMENTS PASSED TO SLOT TO GO HERE! -->
<div class="readmore__wrapper" :class="{ 'active': open }">
<slot></slot>
</div>
Read {{ open ? lessLabel : moreLabel }}
</div>
</template>
<script>
export default {
name: 'read-more',
data() {
return {
open: false,
moreLabel: 'more',
lessLabel: 'less'
};
},
methods: {
toggle() {
this.open = !this.open;
}
},
}
</script>
You can certainly do what you describe. Manipulating the DOM in a component is typically done in the mounted hook. If you expect the content of the slot to be updated at some point, you might need to do the same thing in the updated hook, although in playing with it, simply having some interpolated content change didn't require it.
new Vue({
el: '#app',
components: {
readMore: {
template: '#read-more-template',
data() {
return {
open: false,
moreLabel: 'more',
lessLabel: 'less'
};
},
methods: {
toggle() {
this.open = !this.open;
}
},
mounted() {
const readmoreEl = this.$el.querySelector('.readmore__wrapper');
const firstEl = readmoreEl.querySelector('*');
this.$el.insertBefore(firstEl, readmoreEl);
}
}
}
});
.readmore__wrapper {
display: none;
}
.readmore__wrapper.active {
display: block;
}
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.min.js"></script>
<div id='app'>
Hi there.
<read-more>
<div>First div inside</div>
<div>Another div of content</div>
</read-more>
</div>
<template id="read-more-template">
<div class="readmore">
<!-- SOME ELEMENTS PASSED TO SLOT TO GO HERE! -->
<div class="readmore__wrapper" :class="{ 'active': open }">
<slot></slot>
</div>
Read {{ open ? lessLabel : moreLabel }}
</div>
</template>