Append child to $slot.default - vue.js

I have a component that I need display some custom modal on screen. I don't know where I should put this dialog content, so I did something like that:
<template>
<div class="ComponentItself">
<div v-show="false" ref="ModalContent">
Hello!
</div>
<button v-on:click="showModal">Show modal</button>
</div>
</template>
[...]
Note: I could not set the tag name of [ref=ModalContent] to template because the vue reserves this tag to another feature.
My idea is when I click on "show modal" it open creates an instance of another component (v-dialog) that I have created with the [ref=ModalContent] content (it should be compiled to support nested vue components).
import Dialog from './Dialog';
const DialogCtor = Vue.extend(Dialog);
const dialog = new DialogCtor({ propsData: {...} });
dialog['$slots'].default = [ this.$refs['templateNewFolder'].innerHTML ];
{something like document.body.appendChild(dialog.$el)}
This another component have a slot that could receives the HTML content to be displayed inside of that. And it just not works. The modal is displayed, but the slot content is undefined or the HTML content not parsed.
<div class="Dialog">
[...]
<slot></slot>
[...]
</div>
The current result is something like:
What I need:
I need to know if I am on the right way. I have about the component feature, but I could not identify or understand if it is/could resolve my problem;
What I could do to make it work;
Some similar project could help it, but I could not found anyone;
Maybe I could resolve my problem if is possible I just .appendChild() directly to $slot.default, but it is not possible;

It seems to me this might be a case of an XY problem.
What probably happens is that you do not need to manually fill $slot.default, but use your Dialog component a more standard way. Since there is little detail about the latter in your question, that component might also need some refactoring to fit this "standard way".
So a more standard approach would be to directly use your <custom-dialog> component in the template of your parent, instead of using a placeholder (the one you reference as ModalContent) that you have to hide. That way, whatever HTML you pass within that <custom-dialog> will be fed into your Dialog's <slot> (designed beaviour of slot).
That way you also save the hassle of having to manually instantiate your Dialog component.
Then you can toggle your <custom-dialog> visibility (with v-if or v-show) or even manipulate its position in the DOM as you mention in your code; you can access its DOM node as $el: this.$refs.ModalContent.$el when ModalContent is a Vue instance.
You could also factorize the showModal method by delegating it to the Dialog component.
Code example:
Vue.component('modal-dialog', {
template: '#modal-dialog',
data() {
return {
modalShown: false,
};
},
methods: {
showModal() {
this.modalShown = true;
},
hideModal() {
this.modalShown = false;
},
},
});
new Vue({
el: '#app',
methods: {
showModal() {
this.$refs.ModalContent.showModal();
},
},
});
/*
https://sabe.io/tutorials/how-to-create-modal-popup-box
MIT License https://sabe.io/terms#Licensing
*/
.modal {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
opacity: 0;
visibility: hidden;
transform: scale(1.1);
transition: visibility 0s linear 0.25s, opacity 0.25s 0s, transform 0.25s;
}
.modal-content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: white;
padding: 1rem 1.5rem;
width: 24rem;
border-radius: 0.5rem;
}
.close-button {
float: right;
width: 1.5rem;
line-height: 1.5rem;
text-align: center;
cursor: pointer;
border-radius: 0.25rem;
background-color: lightgray;
}
.close-button:hover {
background-color: darkgray;
}
.show-modal {
opacity: 1;
visibility: visible;
transform: scale(1.0);
transition: visibility 0s linear 0s, opacity 0.25s 0s, transform 0.25s;
}
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<div id="app">
<modal-dialog ref="ModalContent">
Hello!
</modal-dialog>
<h1>Hello World</h1>
<button v-on:click="showModal">Show modal</button>
</div>
<template id="modal-dialog">
<div class="modal" :class="{'show-modal': modalShown}" #click="hideModal">
<div class="modal-content">
<span class="close-button" ref="closeButton" #click="hideModal">×</span>
<slot></slot>
</div>
</div>
</template>
Now if you really want to fiddle with $slot, #Sphinx's linked answer in the question comments is an acceptable approach. Note that the accepted answer there also favours the standard usage. It seems to me this is also what #Sphinx implies in their 2nd comment.

Related

Super small Vue button

I'm learning Vue, and even with the simplest examples there is something wrong. For example, buttons. I have a defined component, myButton, responds to clicks, but it doesn't look like it should, is super small and dont have any label. What am I doing wrong?
Part of index.js:
Vue.component('mybutton', {
props: {
buttonLabel: String,
},
template: '<button #click="onClick()" class="btn">{{ buttonLabel }}</button>',
methods: {
onClick(){
console.log('Click');
}
},
})
Part of index.html:
<div id="app">
<mybutton text="From Vue"></mybutton>
<button class="btn">Test</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="index.js"></script>
And CSS:
.btn {
display: inline-block;
background: #000;
color: #fff;
border: none;
padding: 10px,20px;
border-radius: 5px;
text-decoration: none;
font-size: 15px;
font-family: inherit;
}
Your prop is called buttonLabel, while you pass a property called text inside your index.html. Therefore, the button doesn't get any text and then it's rendered without any inner content (and therefore slim, since you didn't give it fixed width and height).
You need to change the part of index.html and replace text with button-label (Vue automatically maps buttonLabel to it, and it is the better option. Using buttonLabel might not work in this case, since you are not using single file components.
Call it like
<mybutton mylabel="hI"></mybutton>
Vue.component('mybutton', {
props: ['mylabel'],
template: '<button>{{ mylabel }}</button>'
})
https://codepen.io/flakerimi/pen/wvgGqVb
https://v2.vuejs.org/v2/guide/components.html

How to design a reusable dialog box within children components in Vue?

I've been struggling with implementing a dialog box / modal design and behavior from inside of children components in Vue.
So here's the set up, I have a Vue component called "WorkersComponent". This component is just a list of workers assigned to some case fetched from the backend (Laravel). This component is reusable an can be in any place/case/ticket/lookup where a user would want to add workers to.
The component has an "add" button in it. Once clicked, I want a new component to appear at that location (at the click location), which could be a dropdown, modal, dialogue - doesn't really mater. This subcomponent has a search bar and some controls to fetch workers info and add them to the parent component.
My problem is that I can't figure out how to get the nesting / positioning to work. Because it is a child component, its position is always against the parent component, so I can only control it's position within that parent component, but I want it to be displaying on top of other DOM elements and components if necessary - whatever makes sense. Worst case scenario - I want it to be in the middle of the page at least.
Now how do I implement this? I probably want it to be a unique subcomponent, not a global generic modal. On top of it, if it were a global generic, then I have an idea of how to populate the modal with relevant options but how to pass them back to the component that called the modal - no idea. So I'm struggling with the approach. It seems like such a simple thing and yet, I can't find a viable solution.
<workers-component name="Assigned Workers">
<button <!-- Vue controls in here to invoke a modal/dialogue/dropdown --> >Add Worker</button>
<!-- The subcomponent itself -->
<workers-select-component />
</workers-component>
Here's an example from Gmail: wherever this search bar is (let's say it's a parent component), if I click on a triangle, it will expand this other pane, which will (1) appear wherever the search bar is and (2) cover other elements to display it and (3) not dismiss the pane until manually dismissed (which is easy but normal Bootstrap dropdowns don't support this).
Here's a solution:
Vue.component('ToggleDialog', {
props: ['state'],
template: `
<button
#click="$emit('toggle', state)"
class="dialog-button"
>
TOGGLE MODAL
</button>
`
})
Vue.component('DialogModal', {
props: ['state'],
template: `
<div
class="dialog-backdrop"
>
<div
class="dialog-button"
>
<toggle-dialog
:state="state"
#toggle="toggleModal"
/>
</div>
</div>
`,
methods: {
toggleModal(state) {
this.$emit('toggle', state)
}
}
})
new Vue({
el: "#app",
data() {
return {
isModalOpen: false
}
},
methods: {
toggleModal(state) {
this.isModalOpen = !state
}
}
})
.dialog-backdrop {
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
height: 100%;
width: 100%;
background: rgba(0, 0, 0, 0.3);
display: flex;
justify-content: center;
align-items: center;
}
.dialog-button {
padding: 10px 15px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<toggle-dialog :state="isModalOpen" #toggle="toggleModal">
OPEN MODAL
</toggle-dialog>
<dialog-modal v-if="isModalOpen" :state="isModalOpen" #toggle="toggleModal" />
</div>
As you can see the modal is not the child of the button, but the child of the main app. toggle events are emitted (and the modal re-emits it) to the app that controls the state of the modal dialog.
For more complex apps it might not be the best. You could use an event bus (deprecated in Vue3) or Vuex (state management) to overcome this multiple emit-re-emit stuff.
EDIT: NEW SOLUTION
Vue.component('ToggleDialog', {
data() {
return {
isModalOpen: false
}
},
template: `
<div
class="toggle-modal-wrapper"
>
<button
#click="isModalOpen = !isModalOpen"
class="dialog-button"
>
TOGGLE MODAL
</button>
<dialog-modal
v-if="isModalOpen"
#toggle="isModalOpen = !isModalOpen"
>
<slot></slot>
</dialog-modal>
</div>
`
})
Vue.component('DialogModal', {
props: {
innerComponent: {
type: String
}
},
template: `
<div
class="dialog-backdrop"
>
<div>
<slot></slot>
<br />
<button
#click="$emit('toggle')"
class="dialog-button"
>
TOGGLE MODAL
</button>
</div>
</div>
`
})
new Vue({
el: "#app",
})
.toggle-modal-wrapper {
z-index: 10000;
}
.dialog-backdrop {
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
height: 100%;
width: 100%;
background: rgba(0, 0, 0, 0.3);
display: flex;
justify-content: center;
align-items: center;
}
.dialog-button {
padding: 10px 15px;
}
.other-part {
z-index: 1000;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<toggle-dialog>
<template>
This is the first.
</template>
</toggle-dialog>
<toggle-dialog>
<template>
This is the other.
</template>
</toggle-dialog>
<div class="other-part">
OTHER PART OF THE UI
</div>
</div>
You could try playing with slots if you want a reusable component - or even better: the render function.

How to prevent Vue.js hidden element pop-in on page load?

I'm new to Vue.js and I have a (block) element that should be initially hidden on page load. I'm coming from a pure JS mixed with JQuery background so normally I would initially set display:none on the element use JQuery's show/hide methods etc.
I have the showing and hiding working correctly with Vue but a side effect is that the element flashes on the screen briefly on page load until the Vue setup is complete and it knows to hide the element. Setting display:none breaks the show/hide presumably because the elements class prop has higher precedence. Setting opacity:0 also seems to be overriding anything Vue is doing so that breaks the show/hide too. !important on the Vue animation classes does not help either.
The embedded sandbox below might not be the best way to reproduce this, and I suppose it might be system dependent too (speed, memory etc.) but surely this must be a common enough situation with some solution that I've missed.
VUE = new Vue({
el: '#app',
data: {
showFullpageSpinner: false
}
});
setTimeout(function() {
VUE.showFullpageSpinner = true;
setTimeout(function() { VUE.showFullpageSpinner = false; }, 1500);
}, 1500);
.fullpage-spinner-underlay {
position: fixed;
width: 100%;
height: 100%;
left: 0;
top: 0;
background: rgba(0,0,0,0.65);
z-index: 9999;
}
.fullpageSpinner-enter-active, .fullpageSpinner-leave-active {
transition: opacity .25s;
}
.fullpageSpinner-enter, .fullpageSpinner-leave-to {
opacity: 0;
}
.css-spinner {
position: absolute;
display: inline-block;
}
.css-spinner:before {
content: 'Loading...';
position: absolute;
}
.css-spinner:not(:required):before {
content: '';
border-radius: 50%;
border-top: 3px solid #daac35;
border-right: 3px solid transparent;
animation: spinner .7s linear infinite;
-webkit-animation: spinner .7s linear infinite;
}
#keyframes spinner {
to {-ms-transform: rotate(360deg);}
to {transform: rotate(360deg);}
}
#-webkit-keyframes spinner {
to {-webkit-transform: rotate(360deg);}
to {transform: rotate(360deg);}
}
#-moz-keyframes spinner {
to {-moz-transform: rotate(360deg);}
to {transform: rotate(360deg);}
}
.fullpage-loading-spinner {
left: 50%;
top: 45%;
margin-left: -40px;
margin-top: -55px;
}
.fullpage-loading-spinner:BEFORE {
width: 55px;
height: 55px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<transition name="fullpageSpinner">
<div v-if="showFullpageSpinner" class="fullpage-spinner-underlay">
<div class="css-spinner fullpage-loading-spinner"></div>
</div>
</transition>
</div>
Your problem seems to be solvable with the v-cloak directive.
This directive will remain on the element until the associated Vue instance finishes compilation. Combined with CSS rules such as [v-cloak] { display: none }, this directive can be used to hide un-compiled mustache bindings until the Vue instance is ready.
Example:
[v-cloak] {
display: none;
}
<div v-if="showFullpageSpinner" class="fullpage-spinner-underlay" v-cloak>
<div class="css-spinner fullpage-loading-spinner"></div>
</div>

How to close a modal by clicking on the backdrop in Vue.js

I have created a simple reusable modal component using Vue.js and it works fine, but I want to make so that when I click on the backdrop the modal closes, how can I achieve this? I searched and found a similar question on stackoverflow:
vuejs hide modal when click off of it
And did the same that the accepted answer does, putting #click="$emit('close')" on the wrapper but the modal does not get closed by clicking the backdrop as it is in the provided example. Here is my code:
<template>
<div :class="backdrop" v-show="!showModal">
<div class="modal-wrapper">
<div class="modal-container" :class="size" #click="$emit('close')">
<span class="close-x" #click="closeModal">X</span>
<h1 class="label">{{label}}</h1>
<div class="modal-body">
<slot></slot>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'custom-modal',
data() {
return {
showModal: false
};
},
props: {
label: String | Number,
size: String,
backdrop: String
},
components: {
'custom-btn': customBtn
},
methods: {
closeModal() {
this.showModal = true;
}
}
};
</script>
<style>
.modal-wrapper {
display: table-cell;
vertical-align: middle;
}
.modal-container {
margin: 0px auto;
padding: 20px 30px;
border-radius: 2px;
background-color: #fff;
font-family: Helvetica, Arial, sans-serif;
box-shadow: 0 2px 8px rgba(0, 0, 0, .33);
transition: all .3s ease;
}
.close-x {
color: #00A6CE;
float: right;
}
.close-x:hover {
cursor: pointer;
}
</style>
Without a library you need to set it up like this:
<div class="modal-wrapper" #click="$emit('close')>
<div class="modal-container" :class="size" #click.stop=""></div>
</div>
It looks like you're missing the #click.stop="" which is required. Additionally you want to move the $emit('close') up to the modal-wrapper level.
With a library it may be overkill, but this is something that I have used v-click-outside for.
Vue directive to react on clicks outside an element without stopping the event propagation. Great for closing dialogues, menus among other things.
Simply npm install --save v-click-outside
Then (from the docs):
<div v-click-outside="onClickOutside"></div>
and:
onClickOutside (event, el) {
this.closeModal();
},
Try creating a transparent div that covers all the screen but with a z-index < your modals z-index. Then #click on it, you emit your event to close the modal :) Hope it will hellp
<template>
<div #click="handleBackdropClick" class="backdrop" ref="backdrop">
<div class="modal">
<h1> Modal Title </h1>
<input type="text" />
<p> Modal Content </p>
</div>
</div>
</template>
<style>
.modal {
width: 400px;
padding: 20px;
margin: 100px auto;
background: white;
border-radius: 10px;
}
.backdrop{
top: 0;
position: fixed;
background: rgba(0,0,0,0.5);
width: 100%;
height: 100%;
}
.close{
display: none;
}
</style>
export default {
methods: {
handleBackdropClick(e){
console.log(e)
if (e.path[0].className == "backdrop") {
this.$refs.backdrop.classList.add('close');
}
}
}
}
</script>

How to use Vue transition to expand and shrink a div

Using Vue, I have two divs that I want to toggle with a transition. I want slowly expand the div to my desired width on click, and then shrink on another click. I can get the div to expand on one click, but can't figure out how to shrink it on the second click.
Not really clear if just specifying the width of the divs is enough, or if I also have to specify the same width in the css transition classes.
This fiddle shows what I've been trying: https://jsfiddle.net/vxmh8auo/1/
JS
new Vue({
el: '#app',
data: {
showButton: true
},
methods: {
randomise () { this.n = Math.random() }
},
components:{'input-div':blah}
});
CSS
.interaction {
border: 10px solid lightgreen;
display: flex;
flex: 1 0 auto;
max-height: 225px;
transition: max-height 0.25s ease-out;
}
.default {
width: 20px;
}
.bigger{
width: 200px;
}
.expand-enter-active, .expand-leave-active {
transition-property: width;
transition-duration: 5s;
}
.expand-leave-to {
width: 200px;
}
.expand-enter{
width: 20px;
}
HTML
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<div id="app">
<div class="interaction">
<button #click="showButton=!showButton">
<transition name="expand" mode="out-in">
<div v-if="showButton" v-bind:class="showButton ? 'default':'bigger'" key="small"> B </div>
<div v-else class="bigger" key="big"> Bigger </div>
<!--<input-div><</input-div>-->
</transition>
</button>
</div>
</div>
You can do something like this, add class 'default' and toggle class 'bigger':
<button #click="toggleBigger">
<transition name="expand" mode="out-in">
<div class="default" v-bind:class="{ bigger: showButton }" key="small"> B </div>
<!--<input-div><</input-div>-->
</transition>
</button>
Add method toggleBigger, its more readable this way:
methods: {
toggleBigger(){
this.showButton = !this.showButton;
}
And add transitions on your css classes directly like this:
.default {
transition: max-height 0.25s ease-out;
width: 20px;
transition-property: width;
}
.bigger{
transition: max-height 0.25s ease-out;
width: 200px;
transition-duration: 5s;
transition-property: width;
}
You can see working fiddle HERE
PS I did not removed unnecessary classes from fiddle.
This seems so hackey and containing a lot of unnecessary classes, but it works: https://jsfiddle.net/df70pk68/
Again, my use case is a button that expands to a component with an input box and then shrinks again. My solution was to immediately make the component opacity zero, then slowly shrink it. The prevents the input box from breaching the border of the shrinking div. I would love it if someone could figure out a more elegant way to do this....
HTML
<div id="app">
<div class="interaction">
<button #click="showButton=!showButton">
<transition name="fade" mode="out-in">
<div v-if="showButton" class="default" key="small"> B </div>
<input-div class="bigger" v-else><</input-div>
</transition>
</button>
</div>
</div>
CSS
.interaction {
border: 10px solid lightgreen;
display: flex;
flex: 1 0 auto;
max-height: 225px;
}
JS
const blah = Vue.component('input-div',{
template: '<div><input type="text" readonly></div>'
});
new Vue({
el: '#app',
data: {
showButton: true
},
components:{'input-div':blah}
});
.default {
width: 20px;
}
.bigger{
width: 250px;
}
.fade-leave-active {
transition: all 5s ease;
}
.fade-leave-to{
width: 300px;
}
.bigger.fade-leave-to{
width:20px;
opacity: 0
}
.bigger.fade-leave-active{
transition: opacity 0s ease;
transition: width 5s ease;
}
maybe you should forget vue's support on this, only use css3 can do that, and very simple.
first ,this is your div's code
<div id="my-div"></div>
#my-div{
transition: width 0.5s; /* this is the key code you need */
}
then, you can change the width of my-div use vue or js-dom or whatever, the magic thing will happen.