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.)
Related
I am trying to mount a component on a function, it works fine. However I've got it setup so that it destroys the div after X amount of seconds. Then when I try and add the compoent again its removed the base div. I'm not sure how to fix this though...
Component:
<template>
<div>
<b-alert show dismissible variant="danger">
<i class="mdi mdi-block-helper mr-2"></i>{{ text }}
</b-alert>
</div>
</template>
<script>
export default {
name: "alertDanager",
props: {
text: null
},
created() {
setTimeout(() => this.destoryEl(), 5000);
},
methods: {
destoryEl() {
this.$destroy();
this.$el.parentNode.removeChild(this.$el);
}
}
};
</script>
Spawning the component in
const DangerAlertExtended = Vue.extend(dangerAlert);
const error = new DangerAlertExtended({ propsData: { text: "Error message" } });
error.$mount("#error");
I'm not sure how to make it stop overwriting the #error div...
Why not let v-if exclude it from the DOM instead of removing the element? Alternatively, it could be hidden but remain in the DOM with v-show (that's probably what I would do, unless there's a specific reason you need for it to not be in the DOM). I think it's generally better to let Vue manage the DOM rather than manipulate yourself.
<template>
<div v-if="showAlert">
<b-alert show dismissible variant="danger">
<i class="mdi mdi-block-helper mr-2"></i>{{ text }}
</b-alert>
</div>
</template>
<script>
export default {
name: "alertDanager",
props: {
text: null,
showAlert: true
},
created() {
setTimeout(() => this.hideAlert(), 5000);
},
methods: {
hideAlert() {
this.showAlert = false
}
}
}
</script>
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>
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
I have am building a Vue app that includes a QuillJS editor in a tab. I have a simple setTab(tabName) Vue method that shows/hides tabs with the v-if directive.
methods: {
setTab: function (tabName) {
this.view = tabName;
if(tabName === 'compose') {
var editor = new Quill('#editor', {
modules: { toolbar: '#toolbar' },
theme: 'snow'
});
}
}
}
My tab is basically like this:
<div id="composer" v-if="tabName === 'compose'">
<!-- toolbar container -->
<div id="toolbar">
<button class="ql-bold">Bold</button>
<button class="ql-italic">Italic</button>
</div>
<!-- editor container -->
<div id="editor">
<p>Hello World!</p>
</div>
</div>
Currently, I'm getting an error because the #editor element does not yet exist when I am calling new Quill(...). How do I delay that QuillJS initialization on the page so that it doesn't happen until after the #editor is already there?
Use mounted hook.
mounted: function () {
// Code that will run only after the
// entire view has been rendered
}
Use this.$nextTick() to defer a callback to be executed after the next DOM update cycle (e.g., after changing a data property that causes a render-update).
For example, you could do this:
methods: {
setTab: function (tabName) {
this.view = tabName;
if(tabName === 'compose') {
this.$nextTick(() => {
var editor = new Quill('#editor', {
modules: { toolbar: '#toolbar' },
theme: 'snow'
});
})
}
}
}
A clean way to do this is not to rely on selectors but make Quill editor a self-contained component:
<template>
<div class="quill-editor">
<!-- toolbar container -->
<div ref="toolbar">
<button class="ql-bold">Bold</button>
<button class="ql-italic">Italic</button>
</div>
<!-- editor container -->
<div ref="editor">
<p>Hello World!</p>
</div>
</div>
</template>
<script>
...
name: "QuillEditor",
mounted() {
this.quill = new Quill(this.$refs.editor, {
modules: { toolbar: this.$refs.toolbar },
theme: 'snow'
});
}
...
</script>
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>