For those internal components that do not need routing, we want to add a rotation transition effect during switching, which is as follows:



We can introduce a wheel component, but there is a problem, often by component will render all the slide out again to switch, thus leading to all resources will trigger loading, it may not be what we are looking forward to, after all, if things need to slide more disposable too many loading images and other resources. So we can simply write one manually, just to meet the requirements.

Now to implement this feature step by step, write a demo that implements the basic switch.

1. Implement the switchover

To set up a construction scaffold with vue-CLI, use the following command:

npm install -g vue-cli
vue init webpack slide-demo Select no for router, etcCopy the code

This creates a Webpack + vue project. Go to the slide-demo directory and look at SRC/app. vue, the file provided with the starter tool, as a component of the entire page. There is also a SRC/Components directory, which is where the child components are stored.

Create 3 components in this directory: task-1.vue, task-2.vue, task-3.vue, and import them from app. vue as shown below:

<script>
// import HelloWorld from './components/HelloWorld'
import Task1 from "./components/task-1";
import Task2 from "./components/task-2";
import Task3 from "./components/task-3";

export default {
    name: 'App'.components: {
        Task1,
        Task2,
        Task3
    }
}
</script>Copy the code

Our data format questions is as follows:

[{index: 1, type: 1, content: ''}, {index: 2, type: 1, content: ''}, 
 {index: 3, type: 2, content: ''}, {index: 4, type: 3, content: ''}]Copy the code

It is an array. Each element in the array represents each question. Each question has a type, such as multiple choice, fill-in-the-blank, true or false, corresponding to tasK-1, Task-2, and Task-3 respectively. The following code (added to app.vue) :

    data() {
        return {
            currentIndex: 0
        };
    },
    created() {
        // Request question data
        this.questions = [
            {index: 1.type: 1.question: ' '}, / *... * /];
    },Copy the code

How do you do that by changing the currentIndex, and then cutting to the next component?

You can use Vue’s custom global component to dynamically change the component by combining its IS attribute, as shown in the following code:

<template>
<div id="app">
    <div class="task-container">
        <component :is="'task-' + questions[currentIndex].type" 
        </component>
    </div>
</div>
</template>Copy the code

When currentIndex increases, it changes: the value in IS changes from task-1 to task-2, task-3, etc., so that component is replaced with the corresponding Task component.

Next, add a button that switches to the next problem, and change the currentIndex in the response function of that button. Pass the data from Question to Component:

<template>
<div id="app">
    <div class="task-container">
        <component :is="'task-' + questions[currentIndex].type" 
            :question="questions[currentIndex]"></component>
        <button class="next-question" @click="nextQuestion">Next question</button>
    </div>
</div>
</template>Copy the code

The response function nextQuestion is implemented as follows:

methods: {
    nextQuestion() {
        this.currentIndex = (this.currentIndex + 1) 
               % this.questions.length; }},Copy the code

For details about how to implement each task, see Task-1. Vue example:

<template> <section> <h2>{{question.index}}. Selector </h2> <div>{{content}}</div> </section> </template> <script> export default {props: ["question"]}</ script>Copy the code

The final effect is as follows (plus the title) :



2. Add the multicast switching effect

This is usually done by putting all slides together into a long horizontal image and then changing the position of the slide inside the display container. For example, flipsnap.js, a jQuery plugin, puts all slides into float: Left, form a long image and change the translate value of the image to switch. The disadvantage of this plugin is no way to cut from the last one back to the first one, one of the ways to solve this problem is constantly moving DOM: every time when cutting a piece of moved to the back of the last photo, thus realize the last point next time back to the first, the purpose of but this move to move to the performance overhead is large, is not very elegant. Jssor Slider also renders all slides and dynamically calculates the translate value of each slide each time it switches, rather than the overall position of the slide, which is relatively elegant without moving DOM nodes. There are also many Vue round-robin plug-ins implemented in a similar manner.

Anyway, the above wheel planting patterns are not applicable in our scenario, one of which is the answer of scene don’t need to cut back on a topic, each question finish will not be able to go back, one of the more important is that we don’t want to one-time renders all the slide, this will cause the resources in each slide trigger loading, For example, the img tag, even though you display it: None, will request loading as long as its SRC is a normal URL. Since Slides tend to be more numerous, this rotation plug-in is not used.

You can also use transition from Vue, but the problem with transition is that when you cut the next one, the last one is gone, because it’s destroyed, you only have the next animation, and you can’t preload the next Slide.

So let’s implement one manually.

My idea is to prepare two slides each time, the first slide is for the current display, the second slide is put behind it and ready to be cut, when the second slide is cut, delete the first slide, and then add the third slide after the second slide, and repeat the process over and over. If instead of using Vue, we add and delete DOM ourselves, that’s fine, and we can do whatever we want. How can this be elegantly implemented using Vue?

Add another component to the previous component. Initially the first component is displayed, and the second component is spelled to the right. When the second component passes, move the first component after the second and change the content to the third slide. And so on. It’s not a good idea to dynamically modify the DOM with Vue, but you can take advantage of the jssor Slider idea by not moving the DOM and simply changing the Component translate value.

Wrap a next-Task class around one of the components. A component with this class means that it is the next one to appear, and it needs translateX(100%), as shown in the code below:

<template>
<div id="app">
    <div class="task-container">
        <component :is="'task-' + questions[currentIndex].type" 
            ></component>
        <component :is="'task-' + questions[currentIndex + 1].type" 
            class="next-task"></component>
    </div>
</div>
</template>

<style>
.next-task {
    display: none;
    transform: translateX(100%)./* Add an animation that will be triggered when the transform value is changed */
    transition: transform 0.5 s ease;
}
</style>Copy the code

This code hides the component with the.next-task class as an optimization because display: None only builds the DOM and does not render.

So the problem is how to switch the next-Task class between the two Components. The next task is on top of the first one when the second one is cut, so it alternates.

If currentIndex is even, o, 2, 4… Next-task is added to component 2, and next-Task is added to component 1 if currentIndex is odd. So we can switch based on the parity of the currentIndex.

The following code looks like this:

<template>
<div id="app">
    <div class="task-container">
        <component :is="'task-' + questions[evenIndex].type" 
            :class="{'next-task': nextIndex === evenIndex}"
            ref="evenTask"></component>
        <component :is="'task-' + questions[oddIndex].type" 
            :class="{'next-task': nextIndex === oddIndex} ref="oddTask"></component>
    </div>
</div>
</template>

<script>

export default {
    name: 'App',
    data() {
        return {
            currentIndex: 0.// The index currently displayed
            nextIndex: 1.// The next index is currentIndex + 1
            evenIndex: 0.// Even index, currentIndex or currentIndex + 1
            oddIndex: 1      // index of odd numbers}; }},Copy the code

The first component displays the even-numbered slide, and the second component displays the odd slide (represented by an evenIndex and oddIndex, respectively). If nextIndex is even, the even-numbered Component will have a next-Task class. And vice versa. Then change the index values in the response function of the next button:

methods: {
    nextQuestion() {
        this.currentIndex = (this.currentIndex + 1) 
            % this.questions.length;
        this._slideToNext();
    },
    // Cut to the next animation
    _slideToNext() {

    }
}Copy the code

The nextQuestion function does a few other things as well, but inside it is called the _slideToNext function, which is implemented as follows:

_slideToNext() {
    // The current slide type (currentIndex + 1)
    let currentType = this.currentIndex % 2 ? "even" : "odd".// Type of the next slide
        nextType =  this.currentIndex % 2 ? "odd": "even";
    // Get the DOM element for the next slide
    let $nextSlide = this.$refs[`${nextType}Task`].$el;
    $nextSlide.style.display = "block";
    // Set the next slide translate value to 0, which was translateX(100%)
    $nextSlide.style.transform = "translateX(0)";
    // Update the data after the animation ends
    setTimeout((a)= > {
        this.nextIndex = (this.currentIndex + 1) 
            % this.questions.length;
        // The original next was the current slide display
        this[`${nextType}Index`] = this.currentIndex;
        // The original Current Slide will display the next slide
        this[`${currentType}Index`] = this.nextIndex;
    }, 500);
}Copy the code

The code changes the display of the next slide to a block and sets its translateX value to 0. The DOM update cannot be triggered immediately until the animation of the next slide is over, so setTimeout is added. Transpose the nextTask class in the callback, add it to the original Current Slide, and place its contents in the next slide. This is done by changing the corresponding index.

This is almost done, but we found a problem, the cut is cut over, there is no animation. This is because changing display: None to display: block will have no animation, so either change visibility: Hidden to Visible, either trigger the animation operation to add $nextTick or setTimeout 0, for performance reasons, use the second option:

$nextSlide.style.display = "block";
// Use setimeout here, because $nextTick is sometimes not animated and is not required
setTimeout((a)= > {
    $nextSlide.style.transform = "translateX(0)";
    // ...
}, 0);Copy the code

After doing this, the next task will be animated, but there is a problem that the next task with an even number will be covered. Because the next task with an even number will be covered by the second component, it will be covered by the second compoent, so it needs to add a z-index:

.next-task { 
    display: none;
    transform: translateX(100%).transition: transform 0.5 s ease;
    z-index: 2;
}Copy the code

This problem is relatively easy to deal with, another problem is not easy to deal with: the animation time is 0.5s, if the user clicked the next problem quickly within 0.5s, the above code execution will have problems, will lead to data confusion. If the button initialization is disabled every time after cutting to the next question, because the current question has not been answered, only after the answer can become clickable, so that 0.5s is enough time, then this situation can not be considered. But what if you need to deal with this situation?

3. Solve the problem of clicking too fast

I think of two methods. The first method is to use a sliding variable to indicate whether the animation is currently being switched. If so, the data is updated directly when the button is clicked, and the setTimeout 0.5s timer is cleared. This method can solve the problem of data confusion, but the effect of switching is not good, or in the middle of switching suddenly stopped, so the experience is not very good.

The second method is to defer switching, which means that if the user clicks too fast, the actions are queued up, waiting to be animated one by one.

We use an array to represent the queue. If the slide is already being animated, the queue will not be animated, as shown in the following code:

methods: {
    nextQuestion() {
        this.currentIndex = (this.currentIndex + 1) 
            % this.questions.length;
        // insert currentIndex at the top of queue
        this.slideQueue.unshift(this.currentIndex);
        // If there is no current slide, the slide is performed
        !this.sliding && this._slideToNext(); }},Copy the code

Each time you click the button currentIndex is inserted into the queue, but not immediately if the currentIndex is already sliding, otherwise the sliding _sliext function is executed:

_slideToNext() {
    // Fetch the next element to be processed
    let currentIndex = this.slideQueue.pop();
    // Type of the next slide
    let nextType =  currentIndex % 2 ? "odd" : "even";
    let $nextSlide = this.$refs[`${nextType}Task`].$el;
    $nextSlide.style.display = "block";
    setTimeout((a)= > {
        $nextSlide.style.transform = "translateX(0)";
        this.sliding = true;
        setTimeout((a)= > {
            this._updateData(currentIndex);
            // If there are currently unprocessed elements,
            // Continue processing, that is, continue to slide
            if (this.slideQueue.length) {
                // Wait until the DOM of both components is updated
                this.$nextTick(this._slideToNext);
            } else {
                this.sliding = false; }},500);
    }, 0);
},Copy the code

Each time this function fetches the currentIndex currently being processed, and then performs the same operation as in Point 2, except that in the asynchronous callback after the end of the 0.5m animation it needs to determine whether the current queue has any unprocessed elements and, if so, continues _sliext until the queue is empty. This execution needs to hang in nextTick because it can’t operate until the DOM of both components is updated.

That’s fine in theory, but it’s still a problem in practice. Here’s how it feels:



We found that some slides had no transition effect and were not optional and irregular. After some investigation, it is found that if the above nextTick is changed to setTimeout, it will be better, and the longer the setTimeout time is, the less the transition effect will be lost. However, this cannot fundamentally solve the problem, and the reason should be that Vue’s automatic DOM update and Transition animation are not compatible. It may be the asynchronous mechanism of Vue, or the JS combination with Transition itself has a problem, but it has not been encountered before, so there is no in-depth investigation. Anyway, forget about animating the Transition from CSS.

If jQuery is used, you can use jQuery animation. If not, you can use native DOM animate functions, as shown in the following code:

_slideToNext(fast = false) {
    let currentIndex = this.slideQueue.pop();
    // Type of the next slide
    let nextType =  currentIndex % 2 ? "odd" : "even";
    // Get the DOM element for the next slide
    let $nextSlide = this.$refs[`${nextType}Task`].$el;
    $nextSlide.style.display = "block";
    this.sliding = true;
    // Use the native animate function
    $nextSlide.animate([
        / / key frames
        {transform: "translateX(100%)"},
        {transform: "translateX(0)"}, {duration: fast ? 200 : 500.iteration: 1.easing: "ease"
    // Returns an Animate object with an onFinish callback
    }).onfinish = (a)= > {
        // Update the data after the animation ends
        this._updateData(currentIndex);
        if (this.slideQueue.length) {
            this.$nextTick((a)= > {
                this._slideToNext(true);
            });
        } else {
            this.sliding = false; }}; },Copy the code

The animate function is used to achieve the same effect as transition, and there is an onFinish animation callback. The code above has also been optimized to shorten the transition animation if the user is clicking quickly, making it cut faster so it looks more natural. This way, we don’t have a problem with transition. The final effect is as follows:



The experience feels more fluid already.

Native Animate is not compatible with IE/Edge/Safari. You can install a Polyfill library such as web-Animation, use some other third-party animation library, or write your own using setInterval.

If you want to add a button that returns to the previous question, you might want to have three Components, the middle one for display, one on each side, ready to be cut. Specific reader can try by oneself.

This mode can be used in addition to the answer scene, multiple email previews, powerpoint presentations, etc. In addition to providing a transition effect, it can also preload the images, audio, video and other resources required by the next Slide in advance, and does not render all of the Slide at once like traditional rotation plug-ins. Suitable for situations where there is a lot of Slide and no complex switching animations are required.


[Extra] “Efficient Front End” has been listed on the market, jingdong, Amazon, Taobao, etc