How does <transition-group> work with Vue.components? - javascript

Suppose I want to use the components to make a list that would disappear if I click on it, and use the transition-group to do the animation part.
The following code can perform well:
HTML:
<transition-group name="testanim">
<p key="1" v-if='open1' #click='open1 = false'>Can You See Me?</p>
<p key="2" v-if='open2' #click='open2 = false'>Can You See Me?</p>
</transition-group>
CSS:
.testanim-enter-active, .testanim-leave-active {
transition: all .5s;
}
.testanim-enter, .testanim-leave-to {
transform: translateX(1rem);
opacity: 0;
}
.testanim-leave-active {
position: absolute;
}
.testanim-move {
transition: all .5s;
}
open1 and open2 are defined in data in Vue.js.
However, the following code would not perform the animation at all.
HTML:
<transition-group name="testanim">
<test-sth key="1"></test-sth>
<test-sth key="2"></test-sth>
</transition-group>
CSS: the same with above
JavaScript:
Vue.component ("test-sth", {
template: "<p v-if='open' #click='open = !open'>Can You See Me?</p>",
data: function () {
return {
open: true,
}
}
})
So the problem is that how I can animate the components inside the transition-group. I've searched for a few hours but did not find some question or documents related to it.
Update:
The key problem is that the animation in the former example that the second sentence move upwards smoothly when the first sentense disappear do not show in the latter one. Although I may put the transition inside the template, That do not solve the problem.
Should I write the whole transition-groupinside the template, or something else...?

When using Vue transitions, for internal reasons, the transition/transition-group components must be in the same template as the state that's being toggled.
Also, Vue components require that there always be a single root element for a component. A v-if breaks this rule because it gives the possibility of the element not being there, if the v-if happens to be false.
To solve your issue, move the transitioning to the test-sth component. Since it manages its own toggling, it should manage its own transitioning as well.
Vue.component("test-sth", {
template: `
<transition name='testanim'>
<p v-if='open' #click='open = !open'>Can You See Me?</p>
</transition>
`,
data: () => ({
open: true,
}),
})
new Vue({
el: "#app",
template: `
<div>
<test-sth></test-sth>
<test-sth></test-sth>
</div>
`,
})
See this fiddle for a working example.

Related

VueJS: Appending a component programatically to a $ref element

I have dynamic list of 'posts' where I wanted to truncate the text (if it goes beyound a certain # of lines) and show a Read More button that users can click to show the entire text.
In VueJS, I decided to attach a ref to the div I want to append the button to (if the text is truncated).
The component is just a button really but it has some stylings and behaviors I want to copy over. The reason why this got more complicated then it needs to (bad thing?) is because I'm doing the truncating with CSS. I understand that using Javascript might have been easier.
So anyways, how can I dynamically add a component to this div (or its parent) using Javascript only? My own reference to the location would be the ref item.
// code after the promise of getting the posts has resolved in the created() hook
.then(() => {
const posts = this.$refs.posts
posts.forEach(p => {
if (this.Overflown(f)) {
// I want to attach a component (AwesomeButtonComponent) to this p div.
}
}
})
And for clarity:
HTML:
<div v-for="post in posts">
<div class="postBody ref="posts">{{ post.body }}</div>
</div>
isOverflown(el) {
return el.scrollHeight > el.clientHeight || el.scrollWidth > el.clientWidth;
}
The CSS that is truncating the text
.postsBody {
white-space: pre-line;
display: -webkit-box;
-webkit-line-clamp: 5;
-webkit-box-orient: vertical;
overflow: hidden;
}
ALTERNATIVE POSSIBILITIES:
This button will only have ONE functionality, so it being a component is not important and adding styling isn't so difficult.
.then(() => {
const announcementBodies = this.$refs.announcementBody;
announcementBodies.forEach(a => {
if (this.isOverflown(a)) {
const button = document.createElement('button');
button.innerText = 'Click Me';
button.onClick = 'doThis';
a.parentElement.appendChild(button);
}
});
In which case the difficult part would be to add a v-on:click directive to that button and then target that specific tag to remove the clamp css attribute.
Following our discussion into comments, I'll show you 2 way you can do this and try to explain the difference between them and let you decide how you will achieve this.
EXAMPLE ONE
The first example is the shortest I could do. This will need every post to have an isOverflow attribute. There is many way to do it client or server side. The other example will not need it.
<div v-for="post in posts">
{{(post.isOverflow == true) ? post.body.substring(0,3)+'...' : post.body}} <button v-on:click="post.isOverflow = !post.isOverflow">{{(post.isOverflow == true) ? 'SHOW MORE' : 'SHOW LESS'}}</button>
</div>
This is not beautiful, but it work and it let you understand that you can manipulate the post inside the v-for. Each button will be automatically associate with the right post, so when you will click it, only the post associated will be affected.
EXAMPLE TWO
The other example i'll give you is by creating a new component for each post. Let's start with the v-for:
<post-component v-for="post in posts" v-bind:key="post.id" v-bind:post="post"></post-component>
And the new component:
<template>
<div v-bind:class="{'postsBody': isOverflow}">
{{post.body}}
<button v-on:click="changeState()">{{(post.isOverflow) ? 'SHOW LESS' : 'SHOW MORE'}}</button>
</div>
</template>
<script>
export default {
props: {
post:{}
},
data() {
return {
isOverflow: true
}
},
methods: {
changeState: function() {
this.isOverflow = !this.isOverflow;
}
},
}
</script>
<style> //Please, put this in a CSS file, it's only for the example purpose.
.postsBody {
white-space: pre-line;
display: -webkit-box;
-webkit-line-clamp: 5;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>
CONCLUSION
In the end, both of them will have the same result. The difference is what you prefer. I tried to show you two different way to let you understand how things work with Vue. Let me know if you need more explanations.

Vue transition group use enter/leave instead of move

Using vue transition-groups, is there a way to trigger the leave + enter transitions instead of the move transitions for moving elements?
It should leave, and enter at the new position instead. The move transition only seems to work with transformations.
Playground: https://codepen.io/anon/pen/WqJEmV
HTML:
<div id="flip-list-demo" class="demo">
<button v-on:click="shuffle">Shuffle</button>
<transition-group name="flip-list" tag="ul">
<li v-for="item in items" v-bind:key="item">
{{ item }}
</li>
</transition-group>
</div>
JS:
new Vue({
el: '#flip-list-demo',
data: {
items: [1,2,3,4,5,6,7,8,9]
},
methods: {
shuffle: function () {
this.items = _.shuffle(this.items)
}
}
})
CSS:
/** Should NOT use this: **/
.flip-list-move {
transition: transform 1s;
}
/** Should use this instead: **/
.flip-list-enter-active, .flip-list-leave-active {
transition: all 1s;
}
.flip-list-enter {
opacity: 0;
transform: translateX(80px);
}
.flip-list-leave-to {
opacity: 0;
transform: translateY(30px);
}
I had a similar issue so I thought I'd post here for anyone else who finds this later:
The solution that worked for me is to utilize the key attribute to force vue to treat the item that moved as a "new" item. That way the enter/leave animations get fired instead of the move.
I forked your pen to show how it can work: https://codepen.io/josh7weaver/pen/eYOXxed?editors=1010
As you can see, one downside to this approach is that you have to increase the complexity of your data model for it to work. i.e. before you had a simple array, after we have an array of objects since each object needs to be responsible for its own key.
There are many different ways you could generate a unique ID for the item you want to trick vue into thinking is "new," but the method I used I just copied and pasted from a public gist.
Hope it helps!
As far as I'm aware:
Enter is triggered when a new item is added
Leave is triggered when an item is removed
Move is triggered when the order changes
If you want to only trigger an enter to leave transition you would have to add/remove an item from the array instead of shuffle.
Here is an example:
shuffle: function () {
this.items = [1,3,5,7,9];
}

Cannot log DOM elements on the mounted hook of vue.js

For some reason, on the mounted hook I cant seem to log DOM elements on the browser while I am potentially lokking to loop through elements or , as my final resort just work on a specific index of a HTML Collection:
the following is the vue component I am having an issue with:
<template>
<full-page ref="fullpage" id="fullpage" :options="options">
<slider class="section" :auto="false">
<slider-item v-animate-css="'fadeIn'">
<h1 class="mainTitle">PROJECT GORILLA</h1>
</slider-item>
<slider-item v-for="bkg in bkgImg" :style="{backgroundSize:'cover',
backgroundImage: 'url(' + bkg + ')'}">
<h1 class="mainTitle">PROJECT GORILLA</h1>
</slider-item>
</slider>
</full-page>
</template>
<script>
import { Slider, SliderItem } from 'vue-easy-slider'
import pinkBkg from '#/assets/img/IMG_2473.jpg'
import redBkg from '#/assets/img/IMG_4674.jpg'
import blueBkg from '#/assets/img/IMG_4716.jpg'
import greenBkg from '#/assets/img/IMG_2013.jpg'
export default {
data(){
return {
options:{
licenseKey:null
},
bkgImg:[pinkBkg,redBkg,blueBkg,greenBkg]
}
},
components: {
Slider,
SliderItem
},
mounted(){
let slides = document.getElementsByClassName("slider-item");
console.log(slides[0]);
}
}
</script>
<style>
.slider-item:nth-of-type(1) { background-color:black;}
.slider-item > .wrap {
display:flex;
justify-content: center;
align-items:center;
}
h1.mainTitle {
position:fixed;
z-index:99;
color:white !important;
}
.slider-item {
z-index:98 !important;
}
</style>
Bare in mind that I am currently using the webpack template for vue-cli. To be honest jQuery has crossed my mind as a last resort but I really don't want to
resort to that because its is important that the application has a decent performance...
in this case console.log returns undefined. But If I copy and past the code on the browser then it would work fine.
When your component is mounted, it doesn't necessarily mean the child components within it are fully rendered.
As you can see in your template, there are no HTML elements with class="slider-item". I imagine these appear later when the SliderItem components are rendered.
What you can do is add a ref attribute to any element or component you want to reference.
For example
<slider-item ref="sliderItem" v-animate-css="'fadeIn'">
<h1 class="mainTitle">PROJECT GORILLA</h1>
</slider-item>
<slider-item ref="sliderItemRepeater" v-for="bkg in bkgImg"
:style="{backgroundSize:'cover', backgroundImage: 'url(' + bkg + ')'}">
<h1 class="mainTitle">PROJECT GORILLA</h1>
</slider-item>
Then in your mounted hook, you can access
this.$refs.sliderItem // the first, non-repeating component
this.$refs.sliderItemRepeater // an array of the repeated components
See https://v2.vuejs.org/v2/guide/components-edge-cases.html#Accessing-Child-Component-Instances-amp-Child-Elements

Announcing information to a screen reader in vue.js using aria-live

I am trying to get a vue component to announce information dynamically to a screen reader when different events occur on my site.
I have it working to where clicking a button will populate a span that is aria-live="assertive" and role="alert" with text. This works decently the first time, however, clicking other buttons with similar behavior causes NVDA to read the previous text twice before reading the new text. This seems to be happening in vue, but not with a similar setup using jquery, so I'm guessing it has something to do with the way vue renders to the DOM.
I'm hoping there is some way to workaround this problem or perhaps a better way to read the text to the user that would not have this issue. Any help is greatly appreciated.
Here is a simple component I set up in a working code sandbox to show the problem I am having (navigate to components/HelloWorld.vue for the code) -- Note: This sandbox has changed per the answer below. Full code for the component is below:
export default {
name: "HelloWorld",
data() {
return {
ariaText: ""
};
},
methods: {
button1() {
this.ariaText = "This is a bunch of cool text to read to screen readers.";
},
button2() {
this.ariaText = "This is more cool text to read to screen readers.";
},
button3() {
this.ariaText = "This text is not cool.";
}
}
};
<template>
<div>
<button #click="button1">1</button>
<button #click="button2">2</button>
<button #click="button3">3</button><br/>
<span role="alert" aria-live="assertive">{{ariaText}}</span>
</div>
</template>
Ok so what I've found works way more consistently is instead of replacing the text in the element with new text, to add a new element to a parent container with the new text to be read. Instead of storing the text as a single string, I am storing it in an array of strings which will v-for onto the page within an aria-live container.
I have built a full component that will do this in various ways as an example for anyone looking to do the same:
export default {
props: {
value: String,
ariaLive: {
type: String,
default: "assertive",
validator: value => {
return ['assertive', 'polite', 'off'].indexOf(value) !== -1;
}
}
},
data() {
return {
textToRead: []
}
},
methods: {
say(text) {
if(text) {
this.textToRead.push(text);
}
}
},
mounted() {
this.say(this.value);
},
watch: {
value(val) {
this.say(val);
}
}
}
.assistive-text {
position: absolute;
margin: -1px;
border: 0;
padding: 0;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
}
<template>
<div class="assistive-text" :aria-live="ariaLive" aria-relevant="additions">
<slot></slot>
<div v-for="(text, index) in textToRead" :key="index">{{text}}</div>
</div>
</template>
This can be used by setting a variable on the parent to the v-model of the component, and any changes to that variable will be read to a screen reader once (as well as any time the parent container becomes tab-focused).
It can also be triggered by this.$refs.component.say(textToSay); -- note this will also be triggered again if the parent container becomes tab-focused. This behavior can be avoided by putting the element within a container that will not receive focus.
It also includes a slot so text can be added like this: <assistive-text>Text to speak</assistive-text> however, that should not be a dynamic/mustache variable or you will encounter the problem in the original question when the text changes.
I've also updated the sandbox posted in the question with a working example of this component.

How to control order of rendering in vue.js for sibling component

I have following kind of code:
<div>
<compA />
<compB />
</div>
How do I make sure that first compA is rendered only after it compB is rendered.
Why I want is I have some dependency on few elements of compA, and style of compB depends on presence of those elements.
Why in details:
I have some complex UI design, where one box will become fixed when you scroll. SO It will not go above the screen when you scroll, it will be fixed once you start scrolling and it start touching the header. So I am using jquery-visible to find if a div with a particular id is visible on the screen, if it is not visible, I change the style and make that box fixed. Following code should give the idea what I am doing:
methods: {
onScroll () {
if ($('#divId').visible(false, false, 'vertical')) { // This is div from the compA, so I want to make sure it is rendered first and it is visible
this.isFixed = false
} else {
this.isFixed = true
}
}
},
mounted () {
window.addEventListener('scroll', this.onScroll() }
},
destroyed () {
window.removeEventListener('scroll', this.onScroll)
}
I dont want to make those in same component as one reason is it dont make sense as the nature of these components, and other I use compA at many places, while compB is specific to only one page. Also layout of these does not allow me to make compB child of compA as suggested in comments.
Any suggestions are welcome.
An option with events:
<!-- Parent -->
<div>
<comp-a #rendered="rendered = true"></comp-a>
<component :is="compB"></component>
</div>
<script>
// import ...
export default {
components: { CompA, CompB },
watch: {
rendered: function (val) {
if (val) this.compB = 'comp-b';
}
},
data() {
return {
rendered: false,
compB: null
}
}
}
</script>
<!-- Component B -->
<script>
export default {
mounted() {
this.$emit('rendered');
}
}
</script>
After going through the edit I realised that the dependency is not data driven but event driven (onscroll). I have tried something and looks like it works (the setTimeout in the code is for demonstration).
My implementation is slightly different from that of Jonatas.
<div id="app">
RenderSwitch: {{ renderSwitch }} // for demonstration
<template v-if='renderSwitch'>
<comp-a></comp-a>
</template>
<comp-b #rendered='renderSwitchSet'></comp-b>
</div>
When the component-B is rendered it emits an event, which just sets a data property in the parent of both component-A and component-B.
The surrounding <template> tags are there to reduce additional markup for a v-if.
The moment renderSwitch is set to true. component-a gets created.

Categories