Explanation
I have an icon in a button that when clicked opens a menu.
If I want to get the menu correctly under button I need to change the height, because there doesn't seem to be another option (margin doesn't work). But then I get A background hover effect while not hovering over the button.
So to understand.
Once I click on the button but don't hover over the button I get the effect.
The shape is because I changed the height en width.
Background-color: transparant !important; Doens't work
This is the component file
<template>
<v-menu
:close-on-click="true"
offset-y
transition="slide-y-transition"
v-model="language_menu_open"
return-value="language_menu_open = false">
<template v-slot:activator="{ on }">
<v-btn
v-on="on"
v-ripple="false"
icon
depressed
class="language-button">
<v-icon>{{functionMenu(language_menu_open)}}</v-icon>
<v-icon style="margin-right: 10px !important;">mdi-web</v-icon>
</v-btn>
</template>
<v-list class="language-dropdown">
<v-list-item
v-for="language in languages"
:key="language.title"
v-ripple="false"
#click="language">
<v-list-item-title>{{language.title}}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</template>
<script>
export default {
name: "Languages",
data() {
return {
language_menu_open: false,
languages: [
{title: "Nederlands"},
{title: "English"},
{title: "Deutsch"}
],
functionMenu: function chevronChanger(menu_state) {
if (menu_state) {
return "mdi-chevron-up"
} else {
return "mdi-chevron-down"
}
}
}
}
}
</script>
<style scoped>
.language-button {
height: 45px !important;
width: 25px !important;
background-color: transparent !important;
}
.language-button:hover {
background-color: transparent !important;
}
.language-button:hover:before {
background-color: transparent !important;
display: none !important;
}
.language-dropdown {
padding: 0 !important;
}
.language-dropdown:hover:before {
background-color: transparent !important;
display: none;
}
</style>
Any help would be appreciated.
You stated that your primary goal is correct positioning of the menu below the button. Instead of changing the height/width of the button, you should use the nudge-top, nudge-bottom, nudge-right, and/or nudge-left props of the v-menu component. These allow you to precisely tune the positioning of the menu. For example, if you want to move the menu down (further below the button), set the nudge-bottom prop to the number of pixels you want it to move. See documentation here. Using this approach, you don't need to modify the hover effect of your button, which will result in a better user experience.
Also, there is one portion of your template that is inefficient. For one of the icons, you have {{functionMenu(language_menu_open)}}. If possible, you should not use methods in a template except as event handlers, because methods break Vue's reactivity engine. In this case, it would be much better to define a computed property, like this:
computed: {
buttonChevron: function() {
if (this.language_menu_open) {
return "mdi-chevron-up"
} else {
return "mdi-chevron-down"
}
}
}
Then, in your template, instead of {{functionMenu(language_menu_open)}}, use {{ buttonChevron }}. This will have the same effect as your current function, but Vue will handle the changes much more efficiently. (In this particular component, the change will be negligible, but it's a good habit to build.) You can read more about computed properties here.
Try This:
.language-button:active {
background-color: transparent !important;
}
Related
I'm trying to achieve a slide transition between two tabs. One tab is supposed to come from the left pushing the other one to the right and the opposite for the other one.
The leave transition goes well but the tab just pop in straight away without starting where it is supposed to...
I have made a CodePen to reproduce what I've tried : Slide transition test on CodePen
Here is the HTML, it is just a div containting 2 buttons that change the visibility of two div that represents my tabs content.
<div id="transition-test" class="demo">
<div class="tabs">
<button v-for="tab in tabs" class="tab" :key="tab.id" #click="selectedTab = tab.id"> {{tab.text}}</button>
<transition name="slide-right">
<div v-show="1 === selectedTab" class="tab1" key="tab1"></div>
</transition>
<transition name="slide-left">
<div v-show="2 === selectedTab" class="tab2" key="tab2"></div>
</transition>
</div>
</div>
In order to do the transition I do have the following css :
.slide-left,
.slide-right{
position: absolute;
}
.slide-right-enter-to,
.slide-right-leave {
opacity: 1;
transform: translateX(0);
}
.slide-right-enter,
.slide-right-leave-to {
opacity: 0;
transform: translateX(100%);
}
.slide-left-enter-active,
.slide-left-leave-active,
.slide-right-enter-active,
.slide-right-leave-active {
transition: all 500ms ease-in-out;
}
.slide-left-enter-to,
.slide-left-leave {
opacity: 1;
transform: translateX(0);
}
.slide-left-enter,
.slide-left-leave-to {
opacity: 0;
transform: translateX(-100%);
}
Does anyone have an idea about what I'm missing here ?
I found the issue. I don't know why in the Vue transition documentation the css class added at enter is v-enter but the class applied in reality is v-enter-from...
this css class :
.slide-left-enter
becomes :
.slide-left-enter-from
Instead of coding it by yourself, you can use npm version of the transition. It will also help you with its API, Guides and Examples, so that you don't have to worry about those.
I have a v-card that I want to animate back and forth with one click.
If I click on an arrow to the left, the card should scroll to the right and then immediately scroll back in from the left. The other button should work the other way around.
The problem is, nothing happens here. What am I doing wrong?
My template:
<v-card>
<v-btn icon #click="back = false">
<v-icon>mdi-arrow-left</v-icon>
</v-btn>
<v-btn icon #click="back = true">
<v-icon>mdi-arrow-right</v-icon>
</v-btn>
</v-card>
<transition :name="back ? 'slide-fade' : 'slide-fade-reverse'">
<v-card max-width="200" class="mx-auto mt-5" height="80">
<span class="d-flex justify-center pt-7">{{back}}</span>
</v-card>
</transition>
My script:
data() {
return {
back: false,
}
},
My css:
/* Prev */
.slide-fade-enter-active {
transition: all .3s ease;
}
.slide-fade-leave-active {
transition: all .3s ease;
}
.slide-fade-enter {
transform: translateX(100px);
opacity: 0;
}
.slide-fade-leave-to {
transform: translateX(-100px);
opacity: 0;
}
/* Next */
.slide-fade-reverse-enter-active {
transition: all .3s ease;
}
.slide-fade-reverse-leave-active {
transition: all .3s ease;
}
.slide-fade-reverse-enter {
transform: translateX(-100px);
opacity: 0;
}
.slide-fade-reverse-leave-to {
transform: translateX(100px);
opacity: 0;
}
I made a Pen for this: https://codepen.io/Tenarius/pen/WNwdEve
In order for leave and enter transition to work, the <transition> element has to have a v-if condition. When it changes from false to true, the element gets inserted into DOM and animates according to enter transition. When the condition changes from true to false, the leaving transition is performed and, when it ends, the element is removed from DOM.
However, you don't have such a condition. You're simply updating the cards contents and expect it to be removed from DOM and replaced by a new one.
In order to achieve the expected functionality you should use a list of cards (which would only contain the currently active card), coupled with using <transition-group> which, internally, uses the same mechanics as transition but the v-if condition is whether the element is part of the collection or not.
In your case, the "collection" would be a filtered list of cards, containing only one card. With this technique, the leaving element gets the leave animation, while the entering element gets the enter animation, as the elements are actually removed and added to DOM, according to changes in your model.
See it working here.
Since a transition needs leave and enter and thus the element has to "disappear" and "reappear", setTimout can be used to build a workaround.
data() {
return {
back: false,
loading: false
}
},
methods: {
loadTimeout() {
this.loading = true
setTimeout(function(){
this.loading = false
}.bind(this), 500);
}
}
The card can then be expanded with v-show="!loading" and the left- and right-buttons have to call the loadTimeout() function.
Working example here
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.
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.
I am using Vuetify and Electron to make an app to help me with certain tasks at my job. I have disable the browserWindow frame and made my header the draggable area with a button to close the window. I am using the electron vuetify template
vue init vuetifyjs/electron
My problem is the scrollbar reaches all the way to the top but I would like it below my fixed header.
I have tried playing with overflow properties on the html, body, app div, and content div tags but i have not been successful.
How would I accomplish this?
This is purely a CSS question really as you can see this behaviour in the browser too with similar layouts. The easiest way to fix this is using a flex layout:
HTML:
<div class="container">
<div class="titlebar"></div>
<div class="content">
<h1>So much content we scroll</h1>
<h1>So much content we scroll</h1>
<!-- etc -->
</div>
</div>
CSS:
body {
margin: 0;
padding: 0;
overflow: hidden;
}
.container {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
}
.titlebar {
background-color: blue;
height: 35px;
flex-shrink: 0;
}
.content {
flex-grow: 1;
overflow-x: auto;
}
Check out this out in this CodePen
I'd like to offer a Vuetify specific answer for this question, this should apply whether or not Electron is involved.
Vuetify's default styles make this a bit more difficult than a simple CSS solution can give you, especially when the layout gets more complex.
For this example I'm using the complex layout from Vuetify's pre-defined themes here
Vuetify ships with an overflow-y: scroll on the html element so the first step is adding an override for this.
html {
overflow: hidden;
}
This will get rid of the bar on the right side that spans the whole height of the app.
Next you will want to set your v-content area as the scrollable area. There are a few gotchas to watch out for when you're setting this area:
Display flex is already declared
Vuetify sets padding in the style attribute so you'll need to override depending on your case
You'll need a margin the height of your header(only matters if you're changing header height from 64px)
You'll need to remove the header height from the height of the content container using calc(Same as above)
If you have a nav drawer on the right side you'll need to bind a class to take care of this.
My CSS for v-content looks like this, you will need an important to override the padding since it is set by Vuetify through style binding:
main.v-content {
width: 100vw;
height: calc(100vh - 64px);
flex-direction: column;
overflow: scroll;
margin-top: 64px;
padding-top: 0 !important;
}
I also have a class bound to the state of the temporary right drawer on the v-content tag in the template, this makes sure that the scroll bar doesn't disappear underneath the right nav drawer when it's open:
<v-content :class="{ draweropen: drawerRight }">
And the CSS for that bound class, once again you'll need an important to remove the default right padding Vuetify puts on v-content when the drawer is open:
.draweropen {
width: calc(100vw - 300px) !important;
padding-right: 0 !important;
}
You can optionally set the flex-direction to column-reverse if your content is bottom loaded like a chat which is what I'm doing in this CodePen Example
I built a little component that wraps the v-main and moves the scrollbar to the main container instead of the default (the entire html).
Simply replace v-main with this and you're done.
<template>
<v-main class="my-main">
<div class="my-main__scroll-container">
<slot />
</div>
</v-main>
</template>
<script>
export default {
mounted: function() {
let elHtml = document.getElementsByTagName('html')[0]
elHtml.style.overflowY = 'hidden'
},
destroyed: function() {
let elHtml = document.getElementsByTagName('html')[0]
elHtml.style.overflowY = null
},
}
</script>
<style>
.my-main
height: 100vh
.my-main__scroll-container
height: 100%
overflow: auto
</style>