I’m facing some trouble with vue transitions, maybe some of you could help me out?! What I want to realise is a simple, lightweight collapse component using vue transitions.
Therefore in my current approach each time the collapse is collapsing the height of it should be set to contentHeight and then immediately after this to 0 (as well as the other way round when expanding). Or to speak in vue transition’s JavaScript hooks:
before-enter: set height to 0
enter: set height to contentHeight
after-enter: set height to auto
before-leave: set height to contentHeight
leave: set height to 0
This is the current approach:
<template>
<transition
#before-enter="beforeEnter"
#enter="enter"
#after-enter="afterEnter"
#before-leave="beforeLeave"
#leave="leave"
#after-leave="afterLeave"
>
<div
v-if="isCollapsed === false"
:style="componentStyles"
class="UiCollapse"
>
<div
ref="content"
class="UiCollapse-content"
>
<slot/>
</div>
</div>
</transition>
</template>
<script>
export default {
name: "UiCollapse",
props: {
isCollapsed: {
type: Boolean,
default: false
}
},
data() {
return {
componentStyles: {
height: undefined
}
};
},
methods: {
beforeEnter() {
this.setHeight(0);
},
enter() {
this.setHeight(this.getContentHeight());
},
afterEnter() {
this.setHeight(undefined);
},
beforeLeave() {
this.setHeight(this.getContentHeight());
},
leave() {
this.setHeight(0);
},
afterLeave() {
this.setHeight(0);
},
setHeight(height) {
this.componentStyles.height = height;
},
getContentHeight() {
return this.$refs.content.scrollHeight + "px";
}
}
};
</script>
<style scoped>
.UiCollapse {
display: block;
height: auto;
overflow: hidden;
}
.UiCollapse.v-enter-active,
.UiCollapse.v-leave-active {
transition: height 0.3s ease;
}
.UiCollapse-content {
display: block;
}
</style>
With this approach dynamic heights of the content and content changes affecting its height should be considered.
I also transferred this to a codesandbox: https://codesandbox.io/s/yq1nw51yq9
What I would expect here was a smooth opening and closing transition (as defined in css), but as you can see in this project this is working only partially and I do not understand why. Does any of you have any ideas?
Maybe this is not the correct approach, I’m open minded for alternative approaches or already existing libs (though I would prefer to understand vue transitions here), but the solutions I found so far were using fix values for max-height or other workarounds I’d like to avoid.
Thanks in advance for your help!
What about an expansion panel component ? I'm not sure what you really wanna do but it may help you at least. https://vuetifyjs.com/en/components/expansion-panels
Related
I just got a really unexpected bug in my sveltekit application and I can't find anything online talking about it
I have a normal sveltekit application but instead of hydrating the new code when navigating to a new page, it just adds the new code on top of the old one, when i refresh the page it removes the old code (from the previous page)
Edit: after a little bit more exploring I realized it only happens on one page, what part of my code could make this happen?
I had the same issue when having a page loading indicator on SvelteKit for internal navigating. Any page DOM modification, to display the loading indicator during the page navigating, caused the paged appearing twice error. The workaround was to not modify the page during the navigating and use only CSS to display, animate and hide the loading indicator.
Here is my loading indicator code and Here is my documentation regarding the issue. I am not sure if this an internal bug in SvelteKit, so I did not file any bug reports, as I do not have a clean repeatable example. You can also see the fixed page loading indicator on the action on this page if you click any of the blockchains.
<script>
/**
* Svelte does not give a load indication if you hit a link that leads to a page with slow load() function.
* Svelte uses internal router, not server-side loading.
* Thus, we need to manually give some indication in the user interface if the loading takes more than a blink of an eye.
*
* The component is originally made for https://tradingstrategy.ai
*
* Based on the original implementation https://github.com/shajidhasan/sveltekit-page-progress-demo by Shajid Hasan.
*
* As this component is absolutely position, you can put it at any part of your __layout.svelte.
*/
import { onDestroy, onMount } from 'svelte';
import { writable } from 'svelte/store';
import { tweened } from 'svelte/motion';
import { cubicOut } from 'svelte/easing';
const navigationState = writable();
const progress = tweened(0, {
duration: 3500,
easing: cubicOut
});
const unsubscribe = navigationState.subscribe((state) => {
// You will always get state=undefined
// event on the server-side rendering, so
// safely ignore it
//console.log("The loading state is", state);
if (state === 'loading-with-progress-bar') {
progress.set(0, { duration: 0 });
progress.set(0.8, { duration: 5000 });
} else if (state === 'loaded') {
progress.set(1, { duration: 1000 });
}
});
onMount(() => {
// progress.set(0.7);
});
onDestroy(() => {
unsubscribe();
});
</script>
<!-- See the (little) documentation of special SvelteKit events here https://kit.svelte.dev/docs#events -->
<svelte:window
on:sveltekit:navigation-start={() => {
// If the page loads fast enough in the preloading state,
// never display the progress bar
$navigationState = 'preloading';
// Delay the progress bar to become visible an eyeblink... only show if the page load takes too long
setTimeout(function() {
// After 250ms switch preloading to loading-with-progress-bar
if($navigationState === 'preloading') {
$navigationState = 'loading-with-progress-bar';
}
}, 500);
}}
on:sveltekit:navigation-end={() => {
$navigationState = 'loaded';
}}
/>
<!--
Make sure the container component is always in the DOM structure.
If we make changes to the page structure during the navigation, we get a page double render error:
https://stackoverflow.com/questions/70051025/sveltekit-adds-new-page-on-top-of-old-one
Not sure if this is a bug or a feature.
Thus, make sure any progress animation is done using CSS only.
-->
<div class="page-progress-bar" class:loaded={$navigationState === 'loaded'} class:preloading={$navigationState === 'preloading'} class:loading={$navigationState === 'loading-with-progress-bar'}>
<div class="progress-sliver" style={`--width: ${$progress * 100}%`} />
</div>
<style>
/* Always stay fixed at the top, but stay transparent if no activity is going on */
.page-progress-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 0.5rem;
background: transparent;
z-index: 100;
opacity: 0;
transition: opacity 0.5s;
}
/* After transitioning from preloading to loading state, make the progress bar visible with CSS transition on opacity */
.page-progress-bar.loading {
opacity: 1;
transition: opacity 0.5s;
}
.progress-sliver {
width: var(--width);
background-color: var(--price-up-green);
height: 100%;
}
</style>
I also saw this issue with the transition directive. The new page loads while the exit animation is playing. I used local transitions to solve this.
https://svelte.dev/tutorial/local-transitions
When requesting the dimensions of an element currently in a CSS transition, jQuery will return the current value of the element's height at the given time of when the dimensions are requested.
While this is right and well, it often isn't what's needed. I have many cases in which I'd like to retrieve the final dimensions that the element will have after the transition, but while the transition is still in progress.
How can I reliably retrieve the dimensions of an element while its still in transition?
$(document).ready(function() {
$('#one').on('click', function() {
$(this).addClass('trans');
$('#output').text($(this).height());
var self = $(this);
setTimeout(function() {
$('#output').text(self.height());
}, 200);
});
});
#output {}
#one {
width: 200px;
height: 200px;
background: red;
transition: all 1s linear;
}
#one.trans {
height: 600px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<div id="output">...</div>
<div id="one"></div>
See this codepen example
The thing about using setTimeout is that you're forced to use a somewhat arbitrary value. You can use requestAnimationFrame, which should force the code to execute after the transition
A lot of times, you need to wait until the second animation frame, so:
requestAnimationFrame(function() {
requestAnimationFrame(function() {
$('#output').text(self.height());
});
});
I'm building a component in ReactJs that shows some text over a image in a page. This text is configured in a configuration manager app that is created with ReactJs too.
I am using vw (viewport width) as units, beacuse this component needs to be responsive. Using those units the size text is always fitted right. And if I resize the windows the text is in the same place.
My problem is the configuration manager where I configure the text that is going to appear, because I am making a preview of this component inside of modal window.
I need to locate correctly and exactly where the text will appear like if it were the orignal render. It needs to be like a minature of the real component.
I am using vw too, but it doesn't work. It appears in another site of the container and also when I resize the window the text moves too.
I tried to use vmin units, but they did not work too.
I need the container to take the reference as if it were the browser window so that the viewport width units use the same measurements like a preview.
Or if you have any other idea to do this.
This is a reduced part of the code in ReactJS. Also the state is charged with test data for understanding the problem
import React, { Component } from 'react';
import '../styles/main.scss';
class PreviewEditor extends Component {
constructor(props) {
super(props);
this.state = {
text: {
text: '',
fontSize: 2,
position: {
vertical: {
key: 'top',
value: 20
},
horizontal: {
key: 'right',
value: 10
}
}
}
}
}
render() {
let object = this.props;
return (
<div>
<div className='container-body'>
<div>
<div className="container-preview">
<img src={object.url}></img>
<div className="conteiner-text" style={{
[this.state.text.position.vertical.key]: this.state.text.position.vertical.value + "vw",
[this.state.text.position.horizontal.key]: this.state.text.position.horizontal.value + "vw",
fontSize: this.state.text.fontSize + "vw",
}}>
<label>{this.state.text.text}</label>
</div>
</div>
</div>
<div className="row">
............ TODO
</div>
</div>
</div>
);
}
}
export default PreviewEditor
And this the css part
.container-preview {
background-color: #2dc5c5;
width: 100%;
position: relative;
}
.container-preview > img{
max-width: 100%;
}
.conteiner-text{
display: inline-block;
position: absolute;
}
vw stands for viewport width, it aims at always setting size relative to the current viewport, no matter the container. If you just want size relative to container width you should rather user percentage sizes.
So, in your case, you could set each container width differently (using vw for the actual container and pixels for the preview, for example) and all their content sizes in percentage to always have the same ratio at different scales.
I have some code to slide out a menu which works:
Vue.set(this.filterStyles, filterIndex, {
display: "block",
height: "auto",
});
let filterValuesElHeight;
Vue.nextTick().then(() => {
let filterValuesEl = document.getElementById('parent-filter-values-' + filterIndex);
filterValuesElHeight = filterValuesEl.clientHeight;
Vue.set(this.filterStyles, filterIndex, {
height: 0,
display: "block"
});
return Vue.nextTick();
}).then(() => {
setTimeout(() => {
Vue.set(this.filterStyles, filterIndex, {
height: filterValuesElHeight + "px",
display: "block"
});
}, 10);
});
Initially the menu is setup with the following rules:
display: none;
height: 0;
transition: all 500ms;
The first Vue.set sets the height to auto so a accurate height can be taken with filterValuesEl.clientHeight
On the next tick the height is returned back to 0 then finally on the last tick it's set to its natural height.
However, it seems Vue.nextTick() isn't enough although I noticed adding an extremely small timeout seems to do the job. This works but feels quite messy. I was hoping someone might have a better solution?
I'm guessing that you're using filterStyles in a template, something like: :style="filterStyles". If that's the case, then
Vue.set(this.filterStyles, filterIndex, {
display: "block",
height: "auto",
});
won't actually set the height to auto immediately. Rather, it updates the component's data, and that update will be reflected in the DOM after a nextTick(). So, in effect, your code is off by one tick.
Stacking a series of nextTick calls one after the other is probably a bad practice, however. If, for example, the component is removed during the series, you'll be left with a dangling reference.
You could avoid all those ticks by just setting the style directly in order to measure its natural height. If the element has ref="filter" attribute, for example, then something like:
this.$refs.filter.style.height = "auto";
filterValuesElHeight = this.$refs.filter.clientHeight;
this.$refs.filter.style.height = "0";
You may have to work out conflicts between the template and the inline JavaScript (I can't tell because you don't show your template.), but that should get you started.
How to read dimensions and move a div that is hidden before Vue transition starts? For example, a user clicks a button and I want to move a hidden div to appear under the button with a fade-in transition. I need to be able to both read the dimensions and move the top/left position of the hidden div before the transition starts.
Let's say I'm using v-show="active" on the div, where active is my reactive data property I want to set to true and be able to move the div before transition starts.
I've tried all these:
Move the div first, then on nextTick set active = true.
Use the javascript hook beforeEnter to try to move the div before transitions start.
Use the javascript hook enter (and 'done' callback) to try to move the div before transition starts.
Tried all the above with updating the DOM immediately with the new position before setting active = true. (In other words, not via data binding, but actually setting element style properties directly like this.$refs.content.style.top = '500px' to avoid any waiting on the virtual DOM.) However, ideally I would like to accomplish this without directly touching the DOM, but using nextTicks instead. Both approaches fail.
Tried with some success with a hacky transition: all .8ms ease-in, top 1ms, left 1ms.
Tried with success with moving the div first then setting active in a setTimeout. This is not the right solution though.
Update
Thanks to the accepted answer I was able to see that I can read dimensions on nextTick (by which time v-show has turned on display). However, it turns out I needed the transition to be all transition all .3s and that would cause the movement to be included. The DOM will gather up all the changes and apply them together, which means they get lumped into the transition that is later added by Vue. The solution ended up being that I needed to make the movements, then trigger the DOM to repaint first, then trigger the v-show to turn on. Here's an example method:
startTransition () {
this.$refs.content.offsetHeight // <-- Force DOM to repaint first.
this.isContentActive = true // <-- Turns on v-show.
},
Use v-bind:style to move your window and it all works as intended.
Update: To check the size of the popup itself, it has to be shown, so I'm using v-show instead of v-if. The first thing I do is make it visible; on the next tick, I can measure it and place it.
new Vue({
el: '.container',
data: {
top: 0,
left: 0,
width: 0,
show: false
},
methods: {
showFloater: function(evt) {
const t = evt.target;
this.show = true;
Vue.nextTick(() => {
const fEl = this.$el.querySelector('.floating');
this.top = t.offsetTop + 30;
this.left = t.offsetLeft;
this.width = fEl.offsetWidth;
setTimeout(() => this.show = false, 1000);
});
}
}
});
.container {
position: relative;
}
.floating {
border: thin solid black;
padding: 3em;
position: absolute;
}
.fade-enter-active, .fade-leave-active {
transition: opacity .5s
}
.fade-enter, .fade-leave-to /* .fade-leave-active in <2.1.8 */ {
opacity: 0
}
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.2.1/vue.js"></script>
<div class="container">
<button #click="showFloater">Could go here</button>
<button #click="showFloater">Or here</button>
<transition name="fade">
<div v-show="show" class="floating" v-bind:style="{
top: top + 'px',
left: left + 'px'
}">
This window is {{width}}px wide.
</div>
</transition>
</div>