Vue responsive infinite-scroll component with Vuex and SCSS
Tags: vue, scss, javascript, vuex
Published: 2020-12-22 18:10:45

**Technologies to use:**
- Vue CLI, [How to install it here](https://cli.vuejs.org/)
- Sass CSS
- Vuex


**Step 1**
Generate a project with vue cli:

```bash
vue create infinite-scroll
```

*Select this option*
```bash
? Please pick a preset: (Use arrow keys)
  tipical (node-sass, babel, router, vuex)
  default (babel, eslint)
> Manually select features
```
*Select these with tab space, then enter*
```bash
? Check the features needed for your project:
 > ◉ Babel
 ◯ TypeScript
 ◯ Progressive Web App (PWA) Support
 ◯ Router
 > ◉ Vuex
 > ◉ CSS Pre-processors
 ◯ Linter / Formatter
 ◯ Unit Testing
 ◯ E2E Testing
```

```bash
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default):
  Sass/SCSS (with dart-sass)
> Sass/SCSS (with node-sass)
  Less
  Stylus
```

**Step 2**

In our **components** folder we will add this 2 components:

*Button.vue*
```vue
<template>
  <button
    v-if="ui !== 'submit'"
    @click="btnClick"
    :type="type"
    :class="ui ? 'btn__' + ui : 'btn__primary'"
  >
    {{ text }}
  </button>
  <button
    v-else
    @click="btnClick"
    formData.action="/submit"
    :type="type"
    :class="ui ? 'btn__' + ui : 'btn__primary'"
  >
    {{ text }}
  </button>
</template>
<script>
export default {
  name: "btn-fc",
  props: ["text", "type", "disabled", "ui"],
  methods: {
    btnClick() {
      this.$emit("btnClick");
    },
  },
};
</script>

```

*CardItem.vue*
```vue
<template>
  <div class="carditem">
    <div class="carditem__image">
      <img :src="pathBase+'futurama'+character.id+'.jpg'" />
    </div>
    <div class="carditem__info">
      <strong class="carditem__title">{{ character.name.first }} {{ character.name.last }}</strong>
      <span>{{ character.occupation }}</span>
      <span>{{ character.species }}</span>
    </div>
  </div>
</template>
<script>
import { mapGetters, mapActions } from "vuex";
export default {
  name: "CardItem",

  props: {
    character: {
      type: Object,
    },
    index: Number,
    showPage: {
      type: Boolean,
      default: false,
    },
  },
  data() {
    return {
      pathBase: "https://res.cloudinary.com/lariicsa/image/upload/v1602727260/futurama/",
      charImages: [
        { name: "Philip", url: "" },
        { name: "Turanga", url: "" },
        { name: "Bender", url: "futurama4_pxj04y.jpg" },
        { name: "Hubert", url: "" },
        { name: "Amy", url: "" },
        { name: "Hermes", url: "" },
        { name: "Carol", url: "" },
        { name: "John", url: "" },
        { name: "Zapp", url: "" },
        { name: "Scruffy", url: "" },
        { name: "Cubert", url: "" },
        { name: "Kif", url: "" },
        { name: "Dwight", url: "" },
        { name: "LaBarbara", url: "" },
      ],
    };
  },

  mounted() {
    this.index;
  },
};
</script>
```

**Step 3**
In a new folder called assets we will add new folder called scss and then add [these scss files](https://github.com/Lariicsa/infinite-scroll/tree/master/src/assets/scss)

And we will modify the **main.js** file

```javascript
import Vue from 'vue'
import axios from "axios";
import VueAxios from "vue-axios";
import App from './App.vue'
import Vuex from 'vuex'
import router from './router'
import store from './store'
import "./assets/scss/index.scss";

Vue.use(axios);
Vue.use(Vuex)
Vue.config.productionTip = false

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

```


**Step 4**
We will create a new folder called *service* and in a **index.js** file we will call our service from a Futurama API from [sampleapis.com](https://sampleapis.com)


```javascript
import axios from "axios";
const headers = {
  "Content-Type": "application/json",
};

const baseURL = () => {
  (axios.defaults.baseURL = "https://sampleapis.com/futurama/api"), headers;
};

const FUTURAMA_SERVICE = axios.create(baseURL());
const GET_DIMENSION = "/characters";

const characterInfo = (idCharacter) => FUTURAMA_SERVICE.get(GET_DIMENSION+idCharacter);
const characterAll = () => FUTURAMA_SERVICE.get(GET_DIMENSION);

export { characterInfo, characterAll };
```
And in our Store folder at file **index.js** we will add next:
```javascript
import Vue from 'vue'
import Vuex from 'vuex'
import { characterInfo, characterAll } from "../service/index"

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    characterInfo: {},
    charactersList: []
  },
  mutations: {
    setCharactersList:(state, payload) => (state.charactersList = payload),
    setCharacterInfo:(state, payload) => (state.characterInfo = payload)
  },
  actions: {
    async getCharactersList({commit}) {
      try {
        const response = await characterAll()
        const list = response.data
        commit("setCharactersList", list)
        console.log('all',list);
      } catch (error) {
        console.log(error.response);
      }
    },

    async getCharactersDimension({ commit }, idCharacter) {
      try {
        const response = await characterInfo(idCharacter)
        console.log('response', response);
      } catch (error) {
        console.log(error.response);
      }
    }
  },

  getters: {
    gtrCharacters(state) {
      return state.charactersList
    }
  }
})
```
**Step 5**
Now we will create our important component **InfiniteScroll.vue**

```vue
<template>
  <div class="infscroll__container">
    <div id="infinite-list" class="infscroll__items">
      <transition name="fade">
        <div class="infscroll__loader-bg" v-show="showLoading">
          <div class="infscroll__loader">{{ textLoading }}</div>
        </div>
      </transition>
      <slot></slot>
      <ButtonFan
        v-show="isMobile"
        @btnClick="loadMore()"
        text="Load more ..."
        ui="primary infscroll"
      />
    </div>
  </div>
</template>
<script>
import ButtonFan from "@/components/Button";
export default {
  name: "InfiniteScroll",

  components: {
    ButtonFan,
  },

  props: {
    showLoading: {
      type: Boolean,
      default: false,
    },
    textLoading: {
      type: String,
      default: "Loading ...",
    },
  },

  data() {
    return {
      isMobile: false,
    };
  },

  mounted() {
    const listElm = document.querySelector("#infinite-list");
    listElm.addEventListener("scroll", (e) => {
      if (
        /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
          navigator.userAgent
        )
      ) {
        if (listElm.scrollTop + listElm.clientHeight >= listElm.scrollHeight) {
          this.isMobile = true;
        }
      } else {
        if (listElm.scrollTop + listElm.clientHeight >= listElm.scrollHeight) {
          this.loadMore();
        }
      }
    });
  },

  methods: {
    loadMore() {
      this.$emit("loadMore");
    },
  },
};
</script>
```

**Step 6**
And finally we will set the Home View in **Home.vue**

```vue
<template>
  <div class="container">
    <div class="row center">
      <h1>Futurama <span>{{cards.length}} from {{gtrCharacters.length}}</span></h1>
    </div>
    <div class="row center">
      <InfiniteScroll :showLoading="loading" @loadMore="loadMore()">
        <div class="row between">
          <CardItem
            v-for="(character, index) in cards"
            :character="character"
            :key="index"
            :index="parseInt(index + 1)"
          />
        </div>
      </InfiniteScroll>
    </div>
  </div>
</template>
<script>
import { mapActions, mapGetters, mapState, mapMutations } from "vuex";
import CardItem from "@/components/CardItem";
import InfiniteScroll from "@/components/InfiniteScroll";

export default {
  name: "Home",
  components: {
    CardItem,
    InfiniteScroll,
  },

  data() {
    return {
      loading: false,
      items: [],
      upto: 6,
      pages: [],
      pageArea: "",
      isMobile: false,
    };
  },

  static: {
    limitScrollItems: 6,
  },

  async created() {
    await this.getCharactersList();
  },

  mounted() {
    this.cards;
  },

  methods: {
    ...mapActions(["getCharactersList"]),

    loadMore() {
      const listItems = document.querySelector("#infinite-list");
      let heightToTop = listItems.scrollTop;
      if (
        /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
          navigator.userAgent
        )
      ) {
        setTimeout((e) => {
          listItems.scrollTo(0, heightToTop);
        }, 1000);
      }

      this.loading = true;
      setTimeout((e) => {
        const card = this.cards.map((item) => {
          this.cards.push(item);
        });
        this.upto += this.$options.static.limitScrollItems;
        this.loading = false;
      }, 800);
    },
  },

  computed: {
    ...mapState(["charactersList"]),
    ...mapGetters(["gtrCharacters"]),

    cards() {
      const card = this.gtrCharacters.slice(0, this.upto).map((item) => {
        return item;
      });
      return card;
    },
  },
};
</script>
```

And that's all folks!

You can check the code here [https://github.com/Lariicsa/infinite-scroll/blob/master/src/components/InfiniteScroll.vue](https://github.com/Lariicsa/infinite-scroll/blob/master/src/components/InfiniteScroll.vue)
And the demo here [Infinite Scroll Futurama](https://futurama-infinite-scroll.netlify.app/)