Building a simple before-after comparison slider with Vue.js v2
Tags: webdev
Published: 2022-05-31 06:03:07

Comparing the state of any object before and after changes has always been the best tool to demonstrate the difference clearly. So here we make a before and after slider using Vue.js v2. And, of course, where to go without tests? We will use the Jest testing framework and the Vue-test-utils utility library for tests.  


![Building a simple before-after comparison slider with Vue](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/au5ncax57506s5immr0i.png)

This page assumes you've already read the [Vue.js documentation](https://vuejs.org/) and familiar with the [Vue CLI](https://cli.vuejs.org/). Read that first if you are new to Vue.js.

The first step is to install _vue-cli_. The next step is to create an application skeleton using the vue create command in the terminal. We'll call it _images-comparison-slider_. So let's go to the required directory, where your project will be located, and write the command:

```
vue create images-comparison-slider
```

In the settings, specify the default options for _the Vue 2, babel, eslint_ project:

![Building a simple before-after comparison slider with Vue](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jlv90imt3ikrjne7l88u.png)

After our application will be created, we will need to open it in the editor and remove unnecessary styles and components from the _src_ folder. Also, rename the component _HelloWorld.vue_ to _BeforeAfter.vue_ and use it in the main _App.vue_ component:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tabzvmu0pn2quja1vuek.png)Building a simple before-after comparison slider with Vue
In the _App.vue_ component, we need to add a div with the _container_ class and add some styles.

As a result, our _App.vue_ component should look something like this:

```
<template>
  <div class="container">
    <BeforeAfter
      :value="50"
      :step="0.01"
      beforeImage="https://d6xkme6dcvajw.cloudfront.net/images/Cases/godee/png/application-before.png"
      afterImage="https://d6xkme6dcvajw.cloudfront.net/images/Cases/godee/png/application-after.png"
    />
  </div>
</template>

<script>
import BeforeAfter from '@/components/BeforeAfter.vue'
export default {
  name: 'App',
  components: {
    BeforeAfter
  }
}
</script>

<style scoped>
.container {
  max-width: 980px;
  margin: 0 auto;
  height: 100vh;
}
</style>
```
_The BeforeAfter.vue_ component should take 4 props: v_alue, step, beforeImage_, and _afterImage_.

_For example, we will enter two links to images of the same size and add some values ​​for the value and the step props._

Let's look at the _BeforeAfter.vue_ component. Together with styles and logic, it looks like this:

```
<template>
  <div ref="imageWrapper" class="images-wrapper">
    <img class="before-image" :src="beforeImage" alt="Before image">
    <div class="compare-overlay" :style="{ width: `${compareWidth}%`}">
      <img class="after-image" :src="afterImage" alt="After image" :style="{ width: `${width}px` }">
    </div>
    <input
      class="compare__range"
      type="range"
      min="0"
      max="100"
      :step="step"
      :value="compareWidth"
      @input="handleInput"
      tabindex="-1"
    />
    <div
      class="handle-wrap"
      :style="{ left: `${compareWidth}%` }"
    >
      <div class="handle">
        <svg
          class="handle__arrow handle__arrow--l"
          xmlns="http://www.w3.org/2000/svg"
          width="24" height="24"
          viewBox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          stroke-width="2"
          stroke-linecap="round"
          stroke-linejoin="round"
        >
          <polyline points="15 18 9 12 15 6"/>
        </svg>
        <svg
          class="handle__arrow handle__arrow--r"
          xmlns="http://www.w3.org/2000/svg"
          width="24"
          height="24"
          viewBox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          stroke-width="2"
          stroke-linecap="round"
          stroke-linejoin="round"
        >
          <polyline points="9 18 15 12 9 6"/>
        </svg>
      </div>
      <span class="handle-line"></span>
    </div>
  </div>
</template>

<script>
export default {
  name: 'BeforeAfter',
  props: {
    value: {
      type: Number,
      default: 50
    },
    beforeImage: {
      type: String,
      default: ''
    },
    afterImage: {
      type: String,
      default: ''
    },
    step: {
      type: Number,
      default: 0.1
    }
  },
  data() {
    return {
      width: null,
      compareWidth: this.value,
    }
  },
  mounted() {
    this.width = this.$refs.imageWrapper.getBoundingClientRect().width
    window.addEventListener('resize', this.resizeHandler)
  },
  destroyed() {
    window.removeEventListener('resize', this.resizeHandler)
  },
  methods: {
    handleInput(e) {
      this.compareWidth = e.target.value
    },
    resizeHandler() {
      this.width = this.$refs.imageWrapper.getBoundingClientRect().width
    }
  }
}
</script>

<style scoped>
.images-wrapper {
  width: 100%;
  position: relative;
}
.compare-overlay {
  position: absolute;
  overflow:hidden;
  height: auto;
  top:0;
}
.before-image,
.after-image {
  width: 100%;
  height: auto;
}
.after-image {
  z-index: 2;
  width: 50%;
  position: relative;
}
.compare__range {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 999;
  right: 0;
  left: 0;
  height: 50px;
  cursor: ew-resize;
  background: rgba(0,0,0,.4);
  opacity: 0;
}
.handle__arrow {
  position: absolute;
  width: 20px;
}
.handle__arrow--l {
  left:0;
}
.handle__arrow--r {
  right:0;
}
.handle-wrap {
  display: flex;
  align-items: center;
  justify-content: center;
  position: absolute;
  top: 50%;
  height: 100%;
  transform: translate(-50%, -50%);
  z-index: 5;
}
.handle {
  display: flex;
  align-items: center;
  justify-content: center;
  color: white;
  background: #FF0000;
  border-radius: 50%;
  width: 30px;
  height: 30px;
}
.handle-line {
  content: '';
  position: absolute;
  top:0;
  width: 2px;
  height: 100%;
  background: #FF0000;
  z-index: 4;
  pointer-events:none;
  user-select:none;
}
@media screen and (max-width: 568px) {
  .handle {
    width: 25px;
    height: 25px;
  }
  .handle__arrow {
    width: 20px;
  }
}
@media screen and (max-width: 480px) {
  .handle {
    width: 15px;
    height: 15px;
  }
  .handle__arrow {
    width: 10px;
  }
}
</style>
```


We'll not delve into the styles of the component, any developer styles it according to their requirements. Let's look at the main logic of the component. We accept 4 props that have been described above: _value, step, beforeImage_, and _afterImage_. The _value_ is needed to specify the initial position of the slider, the _step_ for the step of moving the _slider_, and _beforeImage_, afterImage are the path to two images.

To get the cursor position value, we use the input element, position it in the center vertically, and hide it with style _opacity_: 0.

We hang an event listener on this element, and we will receive a value for further assigning it to the _width_ of the image container in the _handleInput_ method.

To adjust the width of images when the screen width changes, we use an event handler for _window_ and handle the resize event (line 90 in the code above). Of course, we need to remove all event listeners when our component is destroyed (line 94 in the code above). We also should define two options in the data object: _width_ and _compareWidth_. To the _width_ option, let's assign a value: _null_, and to the _compareWidth_, let's take value from props: _this.value_. When our component is mounted, we will set the value for the width taken from the container ref (look at lines 89 and 2 in the code above).

To visually display the border of the cursor movement, we use _the handle-wrap_ and style it as we like.

The main logic is that we take the value from the input and use the _handleInput_ method to overwrite the _compareWidth_ value.

We can simplify our code by getting rid of the handleInput method and just using _v-model_ to two-way bind the input value to the compareWidth. So we can delete the handleInput method and add _v-model_ to the input element. That's all. Everything should work fine! Let's look at the online code editor; the link is below.

The finished slider and the source of code can be viewed in the [online editor](https://codesandbox.io/s/serene-cherry-iht0g2?file=/src/components/BeforeAfter.vue). Also, you can see how this slider performance our [website](https://maddevs.io/case-studies/godee/?utm_sorce=devto). The next step is to cover our component with tests using _the Jest_ engine and _the Vue-test-utils_ utility library.

[![How to quickly validate forms in Vue.js?](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lyuokbnfzabfzy6lvgqm.png)](https://maddevs.io/blog/vue-form-validation/?utm_source=devto)

##Component Test Coverage
We will use the unit test type. You probably don't need to explain what and how to use it. But just in case, we'll do it together.

Vue Test Utils (VTU) is a set of utility functions that simplify testing Vue.js components. It provides some methods to mount and interact with _Vue components_ isolated.

We will install the necessary tools to test our component for that read the documentation by this [link](https://v1.test-utils.vuejs.org/installation/#using-vue-test-utils-with-jest-recommended).

Then, we need to tell Jest to transform _.vue_ files using _vue-jest_. You can do so by adding the following configuration in _package.json_

Our _package.json_ file looks like this:

```
{
  "jest": {
    "moduleFileExtensions": [
      "js",
      "json",
      "vue"
    ],
    "moduleNameMapper": {
      "^@/(.*)$": "<rootDir>/src/$1"
    },
    "transform": {
      ".*\\.(vue)$": "vue-jest",
      ".*\\.(js)$": "babel-jest"
    }
  },
}
```

To run the tests, we need to enter the following command in the terminal while at the project's root: npm run test:unit. It is written in the _package.json_ file in the _scripts_ field

```
"scripts": {
  "serve": "vue-cli-service serve",
  "build": "vue-cli-service build",
  "test:unit": "vue-cli-service test:unit",
  "lint": "vue-cli-service lint"
}
```

If we try to run the command _npm run test-unit_, a message will appear in the terminal about the lack of tests. Therefore let's create a test directory and file. The testing folder and file hierarchy will look like this:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4f4a3irkz92it0so0k0i.png)

For the _Jest_ engine to recognize our test file, the file extension must be testComponent.spec.js. The structure of the test files and the directory inside the unit folder should exactly repeat the structure of our project root directory src to avoid various kinds of confusion.

Let's write tests for the component BeforeAfter.vue. First of all, we should import our component in the test file and import the shallowMount function. _ShallowMount_ function will load the component itself, ignoring the child component(s).

The full of code looks like this:

```
import BeforeAfter from '@/components/BeforeAfter'
import { shallowMount } from '@vue/test-utils'
import 'regenerator-runtime'

const props = {
  afterImage: 'img.jpg',
  beforeImage: 'img.jpg',
  value: 50,
  step: 0.1,
}

const testWidth = 420
const inputValue = 89

jest.spyOn(window, 'removeEventListener').mockImplementation()
jest.spyOn(window, 'addEventListener').mockImplementation()

describe('BeforeAfter component', () => {
  let wrapper

  beforeAll(() => {
    window.addEventListener('resize')
  })

  beforeEach(() => {
    wrapper = shallowMount(BeforeAfter, {
      propsData: props,
    })
  })

  afterEach(() => {
    wrapper = null
  })

  it('should render correctly with images', () => {
    expect(wrapper.props()).toEqual(props)
    expect(wrapper.is(BeforeAfter)).toBe(true)
    expect(wrapper).toMatchSnapshot()
  })

  it('the compareWidth should correctly react to change value of the input', () => {
    const rangeInput = wrapper.find('.compare__range')
    rangeInput.setValue(inputValue)
    rangeInput.trigger('input change')

    setTimeout(() => {
      expect(wrapper.find('.img-wrapper__compare-overlay').attributes().style).toBe(`width: ${inputValue}%;`)
    }, 0)
    expect(wrapper.vm.compareWidth).toBe(`${inputValue}`)
  })

  it('does not fire resize event by default', () => {
    expect(window.innerWidth).not.toBe(testWidth)
  })

  it('updates the window width', () => {
    window.innerWidth = testWidth
    window.dispatchEvent(new Event('resize'))
    expect(window.innerWidth).toBe(testWidth)
  })

  it('should correctly remove resize event listener from window when component is destroyed', () => {
    wrapper = shallowMount(BeforeAfter, {
      propsData: props,
      attachTo: document.body,
    })
    
    wrapper.destroy()

    expect(window.removeEventListener).toHaveBeenCalledTimes(1)
  })
})
```

Next, we need to pass the props. So let's create test props to pass to the component and prepare the test width and value for the input in advance (lines 5–13 of the code above). The next step is to lock by adding and removing event listeners for _window_ (lines 15, 16 of the code above).

Then we need to create the describe block to group together several related tests (see in the [Jest documentation](https://jestjs.io/docs/api#describename-fn) ). After declaring the _wrapper_ variable before all tests, we hang the resize event handler for the window (line 21). The beforeEach code block runs before each unit test and re-initializes the props that will be passed to the BeforeAfter component, we assign the wrapper variable to the component using the _shallowMount_ function with fake props. After each test we redefine wrapper variable to null.

Next, we write separate _"it"_ blocks, which are wrappers for our tests. The description for each test is in the first argument of the _"it" function._

![How to Create an Extensible Parallax Effect Directive for Vue](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q7b2au0epou9ziwqbiax.png)

##Summary
Here we learned how to develop and test before and after slider using Vue.js v2 in your project. With the proper skills, you can modify this code, use it in various cases, and significantly improve the user interface and user experience.

[![CTA](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gewb8fpx4nv3gkblnqqv.png)](https://maddevs.io/services/?utm_source=devto)

_Previously published at [maddevs.io/blog](https://maddevs.io/blog/building-a-simple-before-after-comparison-slider-with-vue/?utm_source=devto)._