How to make transition work with "visibility" but not "display"? - vue.js

The transition element of vue only works with display:none but not visibility:hidden, is there any way to make it work with visibility? I want to get the clientWidth of the element before it shows up, with display:none I can't get that value.
By the way I'm using vue3.
Here is the reproduction demo:
https://codesandbox.io/s/competent-hermann-b1s5q

I'm going to assume, for the sake of argument, that you genuinely do need to use visibility for hiding and that other potential solutions (such as opacity) won't work in your real use case, possibly because they don't prevent user interactions with the element.
However, the assertion in the question is slightly misleading. It isn't really a difference between display and visibility. The real difference here is that the display case is using v-show, which includes special handling for transitions.
The current source code for v-show can be seen here:
https://github.com/vuejs/vue-next/blob/d7beea015bdb208d89a2352a5d43cc1913f87337/packages/runtime-dom/src/directives/vShow.ts
A similar approach can be used to construct a directive that uses visibility. Below is an example. It is based on the code for v-show but I've cut it back to just the code required for this particular use case:
const visible = {
updated(el, { value, oldValue }, { transition }) {
if (!value === !oldValue) {
return
}
if (value) {
transition.beforeEnter(el)
el.style.visibility = ''
transition.enter(el)
} else {
transition.leave(el, () => {
el.style.visibility = 'hidden'
})
}
}
}
Vue.createApp({
data() {
return {
show: true
};
},
methods: {
toggle() {
this.show = !this.show;
}
},
directives: {
visible
}
}).mount('#app')
#app {
text-align: center;
}
.tooltip-enter-active {
transition: transform 0.4s ease-out, opacity 0.3s ease-out;
}
.tooltip-leave-active {
transition: transform 0.35s ease-in, opacity 0.28s ease-out;
}
.tooltip-enter-from {
transition: none;
}
.tooltip-enter-from,
.tooltip-leave-to {
transform: translateY(-30px) scale(0.96);
opacity: 0;
}
<script src="https://unpkg.com/vue#3.0.2/dist/vue.global.prod.js"></script>
<div id="app">
<transition name="tooltip">
<div v-visible="show">
Using visibility
</div>
</transition>
<button #click="toggle">toggle message</button>
</div>
I did also have to make a small CSS change to give the enter transition a kick:
.tooltip-enter-from {
transition: none;
}

You'd probably be better off without <transition> in this case:
const app = Vue.createApp({
data() {
return {
show: true,
};
},
methods: {
toggle() {
const tooltip = this.$refs.tooltip;
this.show = !this.show;
tooltip.classList.toggle("tooltip-show");
},
},
mounted() {
console.log('Tooltip-width: ', this.$refs.tooltip.clientWidth);
},
});
app.mount('#app')
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
.tooltip {
opacity: 0;
transform: translateY(-30px) scale(0.96);
transition: transform 0.35s, opacity 0.25s;
}
.tooltip-show {
opacity: 1;
transform: translateY(0) scale(1);
}
<script src="https://unpkg.com/vue#3.0.2/dist/vue.global.js"></script>
<div id="app">
<div class="tooltip" ref="tooltip">This will work!</div>
<button #click="toggle">toggle tooltip</button>
</div>

Related

conditional render of navbar based on whether user is signed in on AWS amplify

I am attempting to conditionally render a and based on whether or not a user is signed in or not using AWS amplify and Vue 3 for the frontend. I have been able to get it to not render and then render on sign in but when I log back out the navbar elements are still there and should have disappeared. I am new to Vue, so this maybe an easy fix but am unsure. I have tried making using both computed and a watch to try and force update the computed but that is not working. Any help would be much appreciated. The code is below:
<template>
<header>
<nav class="navbar">
<router-link #click="closeMenu" to="/" class="nav-branding"
>Portal</router-link
>
<ul :class="[menuIsOpen ? 'active' : '', 'nav-menu']" v-show="isSignedIn">
<li #click="toggleMenu" class="nav-item">
<router-link to="/pensions" class="nav-link">Pensions</router-link>
</li>
<li #click="toggleMenu" class="nav-item">
<router-link to="/benefits" class="nav-link">Benefits</router-link>
</li>
<li #click="toggleMenu" class="nav-item">
<router-link to="/annual-leave" class="nav-link"
>Annual Leave</router-link
>
</li>
<li #click="signOut" class="nav-item">
<router-link to="/" class="nav-link">Sign Out</router-link>
</li>
</ul>
<div
#click="toggleMenu"
:class="[menuIsOpen ? 'active' : '', 'hamburger']"
v-show="isSignedIn"
>
<span class="bar"></span>
<span class="bar"></span>
<span class="bar"></span>
</div>
</nav>
</header>
<div :class="[menuIsOpen ? 'pushed' : 'static']"></div>
</template>
<script>
import { Auth } from "aws-amplify";
export default {
name: "NavBar",
data() {
return {
menuIsOpen: false,
};
},
methods: {
toggleMenu() {
this.menuIsOpen = !this.menuIsOpen;
},
closeMenu() {
this.menuIsOpen = false;
},
async signOut() {
try {
await Auth.signOut();
// navigate to the login page or another route
this.$router.push("/");
} catch (err) {
console.log(err);
}
},
async isUser() {
try {
await Auth.currentAuthenticatedUser();
return true;
} catch {
return false;
}
},
},
computed: {
isSignedIn() {
return this.isUser();
},
watch: {
isSignedIn() {
this.$forceUpdate();
},
},
},
};
</script>
<style>
header {
height: auto;
background-color: #0d1520;
}
li {
list-style: none;
}
a {
color: white;
text-decoration: none;
}
.navbar {
min-height: 70px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 24px;
}
.nav-menu {
display: flex;
justify-content: space-around;
align-items: center;
gap: 60px;
}
.nav-branding {
font-size: 2rem;
color: #03e9f4;
}
.nav-branding:hover {
text-shadow: 0 0 5px #03e9f4, 0 0 25px #03e9f4, 0 0 50px #03e9f4,
0 0 100px #03e9f4;
}
.nav-link {
transition: 0.7s ease;
color: #03e9f4;
}
.nav-link:hover {
text-shadow: 0 0 5px #03e9f4, 0 0 25px #03e9f4, 0 0 50px #03e9f4,
0 0 100px #03e9f4;
}
.hamburger {
display: none;
cursor: pointer;
}
.bar {
display: block;
width: 25px;
height: 3px;
margin: 5px auto;
transition: all 0.3s ease-in-out;
background-color: #03e9f4;
}
.hamburger:hover .bar {
box-shadow: 0 0 5px #03e9f4, 0 0 25px #03e9f4, 0 0 50px #03e9f4,
0 0 100px #03e9f4;
}
#media (max-width: 768px) {
.static {
transition: all 0.5s ease-in-out;
padding-top: 0;
z-index: 0;
background-color: #151f31;
}
.pushed {
padding-top: 168px;
transition: padding 0.3s ease-in-out;
transition-delay: 0.2;
z-index: 0;
background-color: #151f31;
}
.hamburger {
display: block;
}
.hamburger.active .bar:nth-child(2) {
opacity: 0;
}
.hamburger.active .bar:nth-child(1) {
transform: translateY(8px) rotate(45deg);
}
.hamburger.active .bar:nth-child(3) {
transform: translateY(-8px) rotate(-45deg);
}
.nav-menu {
position: fixed;
left: 100%;
top: 70px;
gap: 0;
flex-direction: column;
background-color: #0d1520;
width: 100%;
text-align: center;
transition: 0.5s;
}
.nav-item {
margin: 16px 0;
}
.nav-menu.active {
z-index: 1;
left: 0;
}
}
</style>
Update
There're way too many "unknowns" in your problem thus it's difficult to give you a working answer. I'll give you hints but please read the Vue docs carefully to understand how to implement them and to also better understand the framework.
As you mentioned in the comments below, you're handling your sign in function in a "sibling" component and that both components are imported in the App.vue.
App.vue
views/
... NavBar.vue
... Login.vue
Option #1
Without the use of a state management, what you can do is move this code:
export default {
data() {
return { isSignedIn: false };
},
methods: {
async isAuthenticated() {
try {
await Auth.currentAuthenticatedUser();
this.isSignedIn = true;
} catch {
this.isSignedIn = false;
}
},
}
async mounted() {
await this.isAuthenticated();
}
}
in your App.vue, then use an emitter in the login component so that your App.vue can "listen" whenever the user has logged in successfully. You can use this.isAuthenticated() as the callback function in the emit event prop. Then, pass the this.isSignedIn state as a prop in your navbar component:
Login.vue
export default {
...
methods: {
signInUser() {
/** your sign in logic */
this.$emit('signIn')
}
}
...
}
App.vue
<!-- Sample template -->
<NavBar :show="isSignedIn" />
<Login #sign-in="isAuthenticated" />
Navbar.vue
export default {
props: ['show'] // you can then pass show in your v-show
}
Option #2
You can also conditionally render the entire navbar component. However, you need to re-structure your templates a bit:
App.vue
<Navbar v-show="isSignedIn"/>
Option #3
Ideally, App should not be responsible for managing the isSignedIn as the only components that use this state are the NavBar and Login components. With that said, consider using Pinia to manage the states between components.

How to display a component when page is loading in nuxt

I am quite new to nuxt, and I need help here.
async asyncData({ params, route }) {
const { data } = await axios.get(
`${process.env.baseUrl}/homes/?search=${
params.search
}&home_status=${1}`
)
return {
homes: data.results,
}
}
I am trying to populate my component with data(using asyncData), but I want my skeleton loader to show if my page is loading. How do I do that in nuxt?
Here is the code for my skeleton loader;
<template>
<div class="placeholder-container">
<div class="placeholder wave">
<div class="square"></div>
<div class="line"></div>
<div class="line"></div>
<div class="line"></div>
</div>
</div>
</template>
<style scoped>
.placeholder-container {
width: 35rem;
margin: 15px auto 15px auto;
}
.placeholder {
padding: 10px;
width: 100%;
// border: 1px solid lightgrey;
display: flex;
flex-direction: column;
}
.placeholder div {
background: #e8e8e8;
}
.placeholder .square {
width: 100%;
height: 22rem;
border-radius: 1rem;
margin: 0 0 10px;
}
.placeholder .line {
height: 12px;
margin: 0 0 10px 0;
}
.placeholder .line:nth-child(2) {
width: 120px;
}
.placeholder .line:nth-child(3) {
width: 180px;
}
.placeholder .line:nth-child(4) {
width: 150px;
}
.placeholder.wave div {
animation: wave 1s infinite linear forwards;
-webkit-animation: wave 1s infinite linear forwards;
background: #f6f7f8;
background: linear-gradient(to right, #eeeeee 8%, #dddddd 18%, #eeeeee 33%);
background-size: 800px 104px;
}
#keyframes wave {
0% {
background-position: -468px 0;
}
100% {
background-position: 468px 0;
}
}
#-webkit-keyframes wave {
0% {
background-position: -468px 0;
}
100% {
background-position: 468px 0;
}
}
</style>
What I normally do without using nuxt, is to create a data variable(loading=true), and change it to false after I finish making the api call, but since asyncData runs in the server, how do I make that work? I will also appreciate it if there is a better way of doing something like this
Placeholder
To display a placeholder component on a particular page
during loading, switch from asyncData to the fetch hook, which exposes the $fetchState.pending flag that is set to true when complete:
<template>
<div>
<MyLoading v-if="$fetchState.pending" />
<MyContent v-else :posts="posts" />
</div>
</template>
<script>
export default {
data() {
return {
posts: []
}
},
async fetch() {
const { data } = await this.$axios.get(...)
this.posts = data
}
}
</script>
Customizing loading progress bar
Nuxt provides a default loading progress bar that appears at the top of the app while a page is loading. You could customize the progress bar's appearance:
// nuxt.config.js
export default {
loading: {
color: 'blue',
height: '5px'
}
}
Or you could specify your own custom loading component instead:
// nuxt.config.js
export default {
loading: '~/components/MyLoading.vue'
}
demo

Multiple custom checkbox components default checked state vuex problem

This is my custom checkbox component:
<template>
<label class="checkbox">
<input
type="checkbox"
:checked="isChecked"
#change="change"
>
<span />
<slot />
</label>
</template>
<script>
export default {
name: 'Checkbox',
model: {
prop: 'selectedValues',
event: 'change'
},
props: {
value: {
type: String,
required: true
},
selectedValues: {
type: Array,
default: null
},
checked: {
type: Boolean,
default: false
}
},
computed: {
isChecked() {
return this.selectedValues.includes(this.value);
}
},
created() {
if(this.checked) {
const selectedValues = this.selectedValues;
selectedValues.push(this.value);
this.$emit('change', selectedValues);
}
},
methods: {
change() {
const selectedValues = Array.from(this.selectedValues).slice();
const found = selectedValues.indexOf(this.value);
if (found !== -1) {
selectedValues.splice(found, 1);
} else {
selectedValues.push(this.value);
}
this.$emit('change', selectedValues);
}
}
};
</script>
<style lang="scss">
label.checkbox {
position: relative;
user-select: none;
display: inline-flex;
cursor: pointer;
input {
display: none;
&:checked ~ span {
background: #EEE;
&:after {
visibility: visible;
}
}
}
span {
width: 25px;
height: 25px;
border: 1px solid #EEE;
display: inline-block;
transition: all linear 0.3s;
margin-right: 5px;
&:after {
content: "";
position: absolute;
top: 3px;
left: 9px;
border-bottom: 3px solid #FFF;
border-right: 3px solid #FFF;
height: 13px;
width: 5px;
transform: rotate(45deg);
visibility: hidden;
}
}
}
</style>
Inside my form:
<Checkbox
v-model="selectedBrands"
value="bmw"
checked
>
BMW
</Checkbox>
<Checkbox
v-model="selectedBrands"
value="audi"
checked
>
Audi
</Checkbox>
<Checkbox
v-model="selectedBrands"
value="mazda"
>
Mazda
</Checkbox>
computed: {
selectedBrands: {
get() {
return this.$store.state.selectedBrands;
},
set(value) {
this.$store.commit('setSelectedBrands', {selectedBrands: value});
}
}
}
Vuex store:
export default new Vuex.Store(
{
strict: process.env.NODE_ENV !== 'production',
state: {
selectedBrands: []
},
mutations: {
setSelectedBrands(state, payload) {
state.selectedBrands = payload.selectedBrands;
},
}
});
This actually works, but I get vuex error: Error: [vuex] do not mutate vuex store state outside mutation handlers.
However I can change the created() hook in my checkbox component like this:
created() {
if(this.checked) {
const selectedValues = Array.from(this.selectedValues).slice();
selectedValues.push(this.value);
this.$emit('change', selectedValues);
}
}
The vuex error will go away, but only the last checkbox component (with checked prop) will be checked (in this example Audi).
My guess is this happens, because components are rendering asynchronically? Would be happy to hear correct explanation.
My goal is to create a custom checkbox component that supports multiple checkbox v-model array binding (using vuex!) + setting the initial checked state.
I've spent many hours trying to figure out the proper solution. Will be very thankful for your time and help! Thank you in advance!
Your approach will not work - you want the custom checkbox to modify its v-model based on its checked property. But all checkboxes will see the same (empty) selectedValues from the store at the time they emit the change event in created hook. So they will all emit arrays with a single value - their own value. The end result will be that only the last checkbox will become selected - since the computed setter in the parent will be called only after all checkboxes have been created.
If you want to get rid of the mutation error and your checkboxes still working - you should not rely on the checked prop to set their initial value but only rely on the v-model. Therefore, if you want to set them all checked initially - set the Vuex state in your parent component.

Window.resize or document.resize which works & which doesn't? VueJS

I am using Vuetable and its awesome.
I am trying to create a top horizontal scroll, which I have done and its working fine. But I need to assign some events on the window.resize.
I created a component such as
<template>
<div class="top-scrollbar">
<div class="top-horizontal-scroll"></div>
</div>
</template>
<style scoped>
.top-scrollbar {
width: 100%;
height: 20px;
overflow-x: scroll;
overflow-y: hidden;
margin-left: 14px;
.top-horizontal-scroll {
height: 20px;
}
}
</style>
<script>
export default {
mounted() {
document.querySelector("div.top-scrollbar").addEventListener('scroll', this.handleScroll);
document.querySelector("div.vuetable-body-wrapper").addEventListener('scroll', this.tableScroll);
},
methods: {
handleScroll () {
document.querySelector("div.vuetable-body-wrapper").scrollLeft = document.querySelector("div.top-scrollbar").scrollLeft
},
tableScroll() {
document.querySelector("div.top-scrollbar").scrollLeft = document.querySelector("div.vuetable-body-wrapper").scrollLeft
}
}
}
</script>
I am calling it above the table such as <v-horizontal-scroll />
I created a mixin as
Vue.mixin({
methods: {
setScrollBar: () => {
let tableWidth = document.querySelector("table.vuetable").offsetWidth;
let tableWrapper = document.querySelector("div.vuetable-body-wrapper").offsetWidth;
document.querySelector("div.top-horizontal-scroll").style.width = tableWidth + "px";
document.querySelector("div.top-scrollbar").style.width = tableWrapper + "px"
}
}
})
And I am calling it when the user component on which Vuetable is being created
beforeUpdate() {
document.addEventListener("resize", this.setScrollBar());
},
mounted() {
this.$nextTick(function() {
window.addEventListener('resize', this.setScrollBar);
this.setScrollBar()
});
},
I want to understand how this resizing event working.
If I change even a single thing in the above code. I am starting to have issues.
Either it doesn't set the width of scroll main div correctly or even this.setScrollBar don't work on resizing.
I am not clear what is the logic behind this and how it is working?

How to create the perfect autosize textarea?

I do auto expanding textarea.
The principle is this: I create a hidden div in which I place the input text, then in the updated () method I define the height of the div and apply the value to the textarea.
But there is one problem - there is text twitching, because First, the text crawls up, and then when the field is expanded, it returns to its place. As if the updated () method works late. By the same principle, I made a text field in the ReactJS there was no such effect.
What can do with it?
How it works: https://jsbin.com/zakavehewa/1/edit?html,css,js,console,output
<template>
<div class="textarea_wrap">
<textarea class="text_input textarea" v-model="value" ref="textarea"></textarea>
<div v-if="autoRow" class="text_input textarea shadow" ref="shadow">{{ value }}!</div>
</div>
</template>
<script>
export default {
props: {
autoRow: {
type: Boolean,
default: false
},
default: String
},
data () {
return {
value: this.default,
}
},
mounted() {
this.updateHeight()
},
updated() {
this.updateHeight()
},
methods: {
updateHeight() {
if (this.autoRow && this.$refs.shadow) {
this.$refs.textarea.style.height = this.$refs.shadow.clientHeight + 5 + 'px'
}
}
}
}
</script>
<style lang="scss" scoped>
.textarea_wrap {
position: relative;
}
.textarea {
line-height: 1.5;
min-height: 31px;
width: 100%;
font-family: inherit;
}
.shadow {
position: absolute;
left: -9999px;
pointer-events: none;
white-space: pre-wrap;
word-wrap: break-word;
resize: none;
}
</style>
Checkout https://github.com/wrabit/vue-textarea-autogrow-directive, it caters for rendering in hidden divs plus copying and pasting text.