DEV Community

Andrey Gaisinskii
Andrey Gaisinskii

Posted on

Reusable slider component with SwiperJS and NuxtJS

Intro

As you can see in the VueJS docs section for SwiperJS it says:

Swiper Vue.js components are compatible only with new Vue.js version 3

And I immediately got upset because at the time this article is being written, Vue 3.0 is still in preview state, and most of the projects are still running on Vue 2.0.

As for me it feels kinda lame to migrate to Vue 3.0 only because of the swiper library, also there are other options like vue-awesome-swiper, but why would you use a wrapper library that is using old SwiperJS.

So... here is make take:

Preparation

Let's quickly bootstrap our project by running npx create-nuxt-app article-nuxt-swiper in the terminal.

Here are all the options that i have chosen in the CLI:
Nuxt CLI options

Now let's move to the directory of our project by running cd article-nuxt-swiper and add some scss by running in the terminal:

using npm:

npm install --save-dev node-sass sass-loader @nuxtjs/style-resources   
Enter fullscreen mode Exit fullscreen mode

using yarn:

yarn add --dev node-sass sass-loader @nuxtjs/style-resources   
Enter fullscreen mode Exit fullscreen mode

and let's add SwiperJS by running:

using npm:

npm install swiper
Enter fullscreen mode Exit fullscreen mode

using yarn:

yarn add swiper
Enter fullscreen mode Exit fullscreen mode

Then I have disabled buefy css import in nuxt.config.js:

// nuxt.config.js
  modules: [
    // https://go.nuxtjs.dev/buefy
    ['nuxt-buefy', { css: false }],
  ],
Enter fullscreen mode Exit fullscreen mode

And added bulma's and buefy's scss like that:

// nuxt.config.js
  css: [
    '@/assets/scss/main.scss'
  ],

  buildModules: [
    // other stuff
    '@nuxtjs/style-resources'
  ],

  styleResources: {
    scss: ['@/assets/scss/_variables.scss']
  },
Enter fullscreen mode Exit fullscreen mode
// @assets/scss/main.scss
@charset "utf-8";

@import "~bulma";
@import "~buefy/src/scss/buefy";

@import "./_swiper.scss"
Enter fullscreen mode Exit fullscreen mode
// @assets/scss/_variables.scss
$fullhd-enabled: false;

@import "~bulma/sass/utilities/_all.sass";
@import "~buefy/src/scss/utils/_all.scss";
Enter fullscreen mode Exit fullscreen mode
// @assets/scss/_swiper.scss
@import '~swiper/swiper.scss';
@import '~swiper/components/navigation/navigation.scss';
@import '~swiper/components/pagination/pagination.scss';
Enter fullscreen mode Exit fullscreen mode

I have also slightly adjusted some other configs for better TypeScript experience:

// package.json
  "lint-staged": {
    "*.{js,vue}": "eslint"
  },
Enter fullscreen mode Exit fullscreen mode

to:

// package.json
  "lint-staged": {
    "*.{ts,js,vue}": "eslint"
  },
Enter fullscreen mode Exit fullscreen mode

in nuxt.config.js

export default {
// your other stuff 
typescript: {
    typeCheck: {
      eslint: {
        files: './**/*.{ts,js,vue}'
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

in tsconfig.json

{
  // your other stuff
  "compilerOptions": {
    // your other stuff
    "types": [
      "@types/node",
      "@nuxt/types",
      "@nuxtjs/axios"
    ]
  },
}
Enter fullscreen mode Exit fullscreen mode

and in the end have installed nuxt-property-decorator by running:

using npm:

npm install nuxt-property-decorator
Enter fullscreen mode Exit fullscreen mode

using yarn:

yarn add nuxt-property-decorator
Enter fullscreen mode Exit fullscreen mode

Slides

Before we hop into the slider itself, let's first quickly create some markup for our slides. We will have three different types of slides and I will put them into article-nuxt-swiper/components/Slider/templates/<name_of_the_slide>.vue

I will just throw some markup at you:

Slide #1:

<template>
  <div
    :style="`background-image: url(${slide.url})`"
    class="slide-with-big-picture"
  >
    <div class="slide-with-big-picture__main">
      <img class="slide-with-big-picture__picture" :src="slide.thumbnailUrl">
    </div>
    <div class="slide-with-big-picture__description">
      <p class="slide-with-big-picture__text">
        {{ slide.title }}
      </p>
    </div>
  </div>
</template>

<script lang="ts">
import { Vue, Component, Prop } from 'nuxt-property-decorator'

import { Slide } from '../../../types/components/slides.interface'

@Component({})
export default class SlideWithBigPicture extends Vue {
  @Prop({ required: true, type: Object }) readonly slide!: Slide
}
</script>

<style lang="scss">
.slide-with-big-picture {
  display: flex;
  position: relative;
  height: 252px;
  justify-content: center;
  align-items: center;
  background-position: 50% 50%;
  background-repeat: no-repeat;
  background-size: cover;
  +tablet-only {
    height: 240px;
  }
  +mobile {
    height: 192px;
  }
  &__main {
    display: flex;
    position: absolute;
    width: 150px;
    height: 150px;
    align-items: center;
    justify-content: center;
    background-color: #fff;
    border-radius: 4px;
    z-index: 3;
  }
  &__bg {
    position: absolute;
  }
  &__picture {
    display: flex;
    justify-content: center;
    align-items: center;
    padding: 15px;
  }
  &__description {
    display: flex;
    flex-direction: column;
    box-sizing: border-box;
    padding: 16px 20px;
    width: 100%;
    height: 94px;
    bottom: 0;
    margin-top: auto;
    background: rgba(32, 42, 37, 0.6);
    color: #fff;
    z-index: 2;
    +mobile {
      height: 74px;
      padding: 12px;
    }
  }
  &__title,
  &__text {
    line-height: 16px;
    +mobile {
      line-height: 12px;
    }
  }
  &__title {
    font-size: 12px;
    margin-bottom: 6px;
    +mobile {
      font-size: 10px;
    }
  }
  &__text {
    font-weight: 500;
    font-size: 16px;
    +mobile {
      font-size: 12px;
    }
  }
}
</style>

Enter fullscreen mode Exit fullscreen mode

Slide #2:

<template>
  <div
    class="slide-with-small-picture"
  >
    <img :src="slide.thumbnailUrl" class="slide-popular-retailer__picture">
  </div>
</template>

<script lang="ts">
import { Vue, Component, Prop } from 'nuxt-property-decorator'

import { Slide } from '../../../types/components/slides.interface'

@Component({})
export default class SlidePopularRetailer extends Vue {
  @Prop({ required: true, type: Object }) readonly slide!: Slide
}
</script>

<style lang="scss">
.slide-with-small-picture {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  background-color: grey;
}
</style>
Enter fullscreen mode Exit fullscreen mode

Slide #3:

<template>
  <div
    class="slide-with-text"
  >
    <span class="slide-with-text__name">{{ slide.title }}</span>
  </div>
</template>

<script lang="ts">
import { Vue, Component, Prop } from 'nuxt-property-decorator'

import { Slide } from '../../../types/components/slides.interface'

@Component({})
export default class SlideWithText extends Vue {
  @Prop({ required: true, type: Object }) readonly slide!: Slide
}
</script>

<style lang="scss">
.slide-with-text {
  display: flex;
  position: relative;
  height: 108px;
  justify-content: center;
  align-items: center;
  z-index: 2;
  background:yellow;
  &::after {
    z-index: 1;
    content: '';
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    background: linear-gradient(180deg, rgba(22, 101, 193, 0.18) 0%, rgba(22, 101, 193, 0.63) 0%, rgba(5, 34, 68, 0.9) 147.22%);
  }
  &__name {
    color: #fff;
    font-weight: bold;
    font-size: 16px;
    line-height: 20px;
    text-align: center;
    z-index: 3;
  }
}
</style>
Enter fullscreen mode Exit fullscreen mode

Slider

For better understanding I will break things up into four parts:

  • markup
  • coding
  • styles
  • and settings for our slider.

Markup

<template>
  <div
    class="slider"
    :class="`slider--${type}`"
  >
    <div
      class="swiper-button-prev"
      :class="`swiper-button-prev--${type}`"
    />
    <div
      class="swiper-button-next"
      :class="`swiper-button-next--${type}`"
    />
    <div
      :class="`swiper-container--${type}`"
      class="swiper-container"
    >
      <div class="swiper-wrapper">
        <div
          v-for="(slide, index) in slides"
          :key="index"
          class="swiper-slide"
        >
          <component :is="getSlide" :slide="slide" />
        </div>
      </div>
      <div class="swiper-pagination" />
    </div>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode
  1. As you can see there is a lot of :class="`someCssClass--${type}`" thing going on. This type thing is a prop that will be passed to our slider component. And I'm using dynamic classes for easier further styling

  2. The swiper-button-prev, swiper-button-next and swiper-container classes are on the same level, but all of them are inside slider class. That is also done for easier styling of the previous and next navigations buttons, because in get started page of SwiperJS documentation those navigation buttons are within swiper-container, thus making navigation buttons harder to style if you want those buttons to be outside of the slider itself

  3. And the third thing that I want to talk about in our markup is the slide <component :is="getSlide" :slide="slide" />. Here I'm using dynamic components to determine which slide component has to be imported based on the type prop that we have passed to our slider component and we also pass a slide prop to the slide with some data that will be displayed in that slide

Coding

I've made some comments in the code, other important stuff will be written below the code. If there is some frustration regarding typescript, please, leave a comment and I will try to help you in my spare time.

<script lang="ts">
// this is needed for typescript, omit if you are using javascript
import { Vue, Component, Prop } from 'nuxt-property-decorator'

// here we import SwiperJS library, you can name the way you want,
// for e.g. - SwiperInstance, SwiperCore or just Swiper
import SwiperInstance, { Navigation, Pagination, A11y } from 'swiper'

// this is needed for typescript, omit if you are using javascript
import { SwiperOptions, Swiper } from 'swiper/swiper.d'
// this is needed for typescript, omit if you are using javascript
import { Slide } from '../../types/components/slides.interface'

// Here we import our settings from a separate .ts file
// We will talk about it a bit later.
import settings from './settings'

// Here we configure out Swiper to use additional modules
SwiperInstance.use([Navigation, Pagination, A11y])

const SlideWithBigPicture = () => import('./templates/SlideWithBigPicture.vue')
const SlideWithSmallPicture = () => import('./templates/SlideWithSmallPicture.vue')
const SlideWithText = () => import('./templates/SlideWithText.vue')

@Component({
  components: {
    SlideWithBigPicture,
    SlideWithSmallPicture,
    SlideWithText
  }
})
export default class Slider extends Vue {
  @Prop({ required: true, type: Array }) readonly slides!: Slide[]
  @Prop({ required: true, type: String }) readonly type!: string

  private swiperInstance: Swiper = {} as Swiper

  private settings: SwiperOptions = settings[this.type]

  get getSlide () {
    switch (this.type) {
      case 'with-small-picture':
        return 'SlideWithSmallPicture'
      case 'with-text':
        return 'SlideWithText'
      case 'with-big-picture':
        return 'SlideWithBigPicture'
      default:
        break
    }
  }

  mounted () {
    this.swiperInstance = new SwiperInstance(`.swiper-container--${this.type}`, this.settings)
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode
  1. As I have already mentioned I'm using dynamic components along with their async importing like that:

    
        const SlideWithBigPicture = () =>         
        import('./templates/SlideWithBigPicture.vue')
        const SlideWithSmallPicture = () => 
        import('./templates/SlideWithSmallPicture.vue')
        const SlideWithText = () => 
        import('./templates/SlideWithText.vue')
    
    

    And then I register them as usual in the components object of
    VueJS:

    
        @Component({
          components: {
            SlideWithBigPicture,
            SlideWithSmallPicture,
            SlideWithText
          }
        })
    
    
  2. Then we define two props in the slider component: type that will tell which slide component to load and slides that is an array of our slides

    
        @Prop({ required: true, type: Array }) readonly slides!: Slide[]
        @Prop({ required: true, type: String }) readonly type!: string
    
    
  3. Then we define two properties: swiperInstance which will hold our SwiperJS object and settings which will hold settings of our slider.

    
        private swiperInstance: Swiper = {} as Swiper
    
        private settings: SwiperOptions = settings[this.type]
    
    

    Also, I want to mention that I do this: settings[this.type],
    I'm doing it because the settings that we import into the slider
    component can be a huge object with a lot of settings for each
    slide type, by accessing only one property from this object we
    are cutting a lot of useless data.

  4. Then we have this:

    
      get getSlide () {
        switch (this.type) {
          case 'with-small-picture':
            return 'SlideWithSmallPicture'
          case 'with-text':
            return 'SlideWithText'
          case 'with-big-picture':
            return 'SlideWithBigPicture'
          default:
            break
        }
      }
    
    

    Our get getSlide () {} is a computed property inside of which there
    is a switch statement that takes our type prop as an argument
    and returns a corresponding VueJS component.

  5. And finally we have this:

    
        mounted () {
            this.swiperInstance = new SwiperInstance(`.swiper-container--${this.type}`, this.settings)
       }
    
    

    Here we are passing our imported SwiperInstance into VueJS
    property and with a class name of our slider as first argument
    and settings for a slider as a second argument.

    We do everything in the mounted hook because
    we need our markup to be already rendered in order for SwiperJS
    to pick it up and initiate.

Styles

Screw this, I'm just throwing some scss at you:

<style lang="scss">
.slider {
  position: relative;
  .swiper-button-next,
  .swiper-button-prev {
    outline: none;
  }
  .swiper-container {
    z-index: unset;
  }
}

.slider--with-big-picture {
  .swiper-button-next,
  .swiper-button-prev {
    @include touch {
      display: none;
    }
    display: inline-flex;
    top: -56px;
    left: unset;
    right: 0px;
    bottom: unset;
    margin: auto;
    width: 32px;
    height: 32px;
    border: 1px solid #000;
    border-radius: 50%;
    outline: none;
    &::after {
      font-size: 10px;
      color: #000;
      font-weight: bold;
    }
  }
  .swiper-button-prev {
    right: 44px;
  }
  .swiper-pagination {
    display: flex;
    position: static;
    justify-content: center;
    margin-top: 20px;
    @include mobile {
      margin-top: 12px;
    }
    .swiper-pagination-bullet {
      margin-right: 8px;
    }
    .swiper-pagination-bullet-active {
      background-color: blue;
    }
  }
}

.slider--with-small-picture,
.slider--with-text {
  @include tablet-only {
    margin-right: -40px;
  }
  @include mobile {
    margin-right: -16px;
  }
  .swiper-pagination {
    display: none;
  }
  .swiper-button-disabled {
    display: none;
  }
  .swiper-button-prev,
  .swiper-button-next {
    @include touch {
      display: none;
    }
    height: 40px;
    width: 40px;
    background-color: #fff;
    border-radius: 50%;
    box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.15);
    &::after {
      font-size: 14px;
      color: #000;
      font-weight: bold;
    }
  }
  .swiper-button-next {
    right: -20px;
  }
  .swiper-button-prev {
    left: -20px;
  }
}

</style>
Enter fullscreen mode Exit fullscreen mode

Settings

So here is out settings object:

// this is needed for typescript, omit if you are using javascript
import { SwiperOptions } from 'swiper/types/swiper-options'

// : { [key: string]: SwiperOptions } is for typescript users
const settings: { [key: string]: SwiperOptions } = {
  'with-small-picture': {
    slidesPerView: 2.5,
    slidesPerGroup: 1,
    slidesOffsetAfter: 16,
    spaceBetween: 8,
    navigation: {
      nextEl: '.swiper-button-next--with-small-picture',
      prevEl: '.swiper-button-prev--with-small-picture'
    },
    breakpoints: {
      769: {
        slidesPerView: 4.5,
        slidesPerGroup: 1.5,
        spaceBetween: 16,
        slidesOffsetAfter: 40
      },
      1024: {
        slidesPerView: 5.5,
        slidesPerGroup: 5.5,
        slidesOffsetAfter: 0,
        spaceBetween: 16
      }
    }
  },
  'with-text': {
    slidesPerView: 1.75,
    slidesPerGroup: 1,
    centeredSlides: true,
    centeredSlidesBounds: true,
    slidesOffsetAfter: 16,
    spaceBetween: 8,
    navigation: {
      nextEl: '.swiper-button-next--with-text',
      prevEl: '.swiper-button-prev--with-text'
    },
    breakpoints: {
      769: {
        slidesPerView: 3.2,
        centeredSlides: false,
        centeredSlidesBounds: false,
        slidesPerGroup: 1.2,
        spaceBetween: 16,
        slidesOffsetAfter: 40
      },
      1024: {
        slidesPerView: 4,
        slidesPerGroup: 4,
        slidesOffsetAfter: 0,
        spaceBetween: 16
      }
    }
  },
  'with-big-picture': {
    slidesPerView: 1,
    spaceBetween: 16,
    pagination: {
      el: '.swiper-pagination',
      clickable: true
    },
    navigation: {
      nextEl: '.swiper-button-next--with-big-picture',
      prevEl: '.swiper-button-prev--with-big-picture'
    },
    breakpoints: {
      769: {
        slidesPerView: 2
      },
      1024: {
        slidesPerView: 3,
        slidesPerGroup: 3
      }
    }
  }
}

export default settings

Enter fullscreen mode Exit fullscreen mode

Our const settings = {} is an object that holds three child objects, each of one has a name of the slide as a key property and contains properties of SwiperJS. As I already said, in Slide.vue we do this: private settings: SwiperOptions = settings[this.type] so we accessing only one child object of settings object.

Final

Well, that's it.

Now we only have to create a page and import our slider with different type props.

<template>
  <main class="page--main">
    <div class="container">
      <slider
        class="page__slider"
        type="with-big-picture"
        :slides="slides"
      />

      <slider
        class="page__slider"
        type="with-small-picture"
        :slides="slides"
      />

      <slider
        type="with-text"
        class="page__slider"
        :slides="slides"
      />
    </div>
  </main>
</template>

<script lang="ts">
import { Vue, Component } from 'nuxt-property-decorator'

import Slider from '../components/Slider/Slider.vue'

import { Slide } from '../types/components/slides.interface'

@Component({
  components: {
    Slider
  },
  async asyncData ({ $axios }) {
    try {
      const response = await $axios.$get('https://jsonplaceholder.typicode.com/photos?_start=0&_limit=10')
      return {
        slides: response
      }
    } catch (error) {

    }
  }
})
export default class MainPage extends Vue {
  private slides: Slide[] = []
}
</script>

<style lang="scss">
.page--main {
  padding: 100px 0px;
  .page {
    &__slider {
      &:not(:last-of-type) {
        margin-bottom: 40px;
      }
    }
  }

  .container {
    @include touch {
      padding: 0px 40px;
    }
    @include mobile {
      padding: 0px 16px;
    }
  }
}
</style>
Enter fullscreen mode Exit fullscreen mode

And voilà! Here we have it!

Links

GitHub repo can be found here - https://github.com/andynoir/article-nuxt-swiper

Live preview can be found here - https://andynoir.github.io/article-nuxt-swiper/

Top comments (3)

Collapse
 
zulvkr profile image
zulvkr

Thank you for the article, it shed some light on async dynamic component.

I think I am left with this as viable option where vue awesome swiper is abandoned and swiper only use pure esm export. Or, maybe shop for other library. I will try to make the JS version of this for an MVP.

Thanks again

Collapse
 
jaka1901 profile image
Jaka Ramadhan

nice article dude, love this so much.
on your code, i seems code like this
+mobile and +tablet-only
how it can be works? i think its great to have some shorthand on css media queries like that. thanks!

Collapse
 
gaisinskii profile image
Andrey Gaisinskii

Hi, sorry for the late reply, these are buefy sass mixins, at first I wanted to use buefy for some styling, but at the end decided not to do so, just used some of those mixins.