DEV Community

Cover image for Infinite Scroll with Svelte 3 and IntersectionObserver 😍
Gustavo Castillo
Gustavo Castillo

Posted on • Updated on

Infinite Scroll with Svelte 3 and IntersectionObserver 😍

Introduction

Recently I've been learning Svelte 3, which is a frontend framework to cybernetically enhanced web apps and I liked it a lot. In this article, I'm going to show you how to build an application with an infinite scroll using IntersectionObserver and Svelte.

In my opinion, the best way of learning a new technology is by doing lots of toy projects, and having fun in the process, so let's get started.

Create the project folder

Execute these commands in your terminal one by one in order to use the Svelte template.

# Install a Svelte project using sveltejs/template
$ npx degit sveltejs/template infinite-scroll

# Change working directory
$ cd infinite-scroll

# Install npm dependencies
$ npm install

# Run the dev server
$ npm run dev
Enter fullscreen mode Exit fullscreen mode

Create component's files

Create a folder called components inside src one, then create four components: Character.svelte, Footer.svelte, Header.svelte, and Loader.svelte, make sure you use Svelte's file extension, which is .svelte.

Implement Footer component

<style>
  .footer {
    display: flex;
    justify-content: center;
    color: #fff;
    padding-top: 3rem;
    padding-bottom: 2rem;
  }

  .footer a {
    color: inherit;
    text-decoration: underline;
  }
</style>

<footer class="footer">
  <span>Built with Svelte 3 original design by &nbsp;</span>
  <a href="https://axelfuhrmann.com/" target="_blank" rel="noopener">
    Axel Fuhrmann
  </a>
</footer>
Enter fullscreen mode Exit fullscreen mode

Implement Header component

<style>
  .header {
    height: 50vh;
    display: flex;
    align-items: center;
    justify-content: center;
    text-transform: uppercase;
  }

  .title {
    font-size: 3.6em;
    display: flex;
    color: var(--text-color);
    align-items: center;
    flex-direction: column;
  }

  .title span {
    display: block;
    background-color: var(--text-color);
    border: medium none;
    font-size: 1.875rem;
    color: var(--orange-color);
    padding: 0 1rem;
    transform: skew(2deg);
  }
</style>

<header class="header">
  <h1 class="title">
    The Rick and Morty APP
    <span>Clone</span>
  </h1>
</header>
Enter fullscreen mode Exit fullscreen mode

Implement Loader component

<style>
  .spinner {
    color: official;
    display: inline-block;
    position: relative;
    width: 40px;
    height: 40px;
  }

  .spinner div {
    transform-origin: 20px 20px;
    animation: spinner 1.2s linear infinite;
  }

  .spinner div:after {
    content: ' ';
    display: block;
    position: absolute;
    top: 1.5px;
    left: 18px;
    width: 1.5px;
    height: 4.5px;
    border-radius: 10%;
    background: #fff;
  }

  .spinner div:nth-child(1) {
    transform: rotate(0deg);
    animation-delay: -1.1s;
  }

  .spinner div:nth-child(2) {
    transform: rotate(30deg);
    animation-delay: -1s;
  }

  .spinner div:nth-child(3) {
    transform: rotate(60deg);
    animation-delay: -0.9s;
  }

  .spinner div:nth-child(4) {
    transform: rotate(90deg);
    animation-delay: -0.8s;
  }

  .spinner div:nth-child(5) {
    transform: rotate(120deg);
    animation-delay: -0.7s;
  }

  .spinner div:nth-child(6) {
    transform: rotate(150deg);
    animation-delay: -0.6s;
  }

  .spinner div:nth-child(7) {
    transform: rotate(180deg);
    animation-delay: -0.5s;
  }

  .spinner div:nth-child(8) {
    transform: rotate(210deg);
    animation-delay: -0.4s;
  }

  .spinner div:nth-child(9) {
    transform: rotate(240deg);
    animation-delay: -0.3s;
  }

  .spinner div:nth-child(10) {
    transform: rotate(270deg);
    animation-delay: -0.2s;
  }

  .spinner div:nth-child(11) {
    transform: rotate(300deg);
    animation-delay: -0.1s;
  }

  .spinner div:nth-child(12) {
    transform: rotate(330deg);
    animation-delay: 0s;
  }

  @keyframes spinner {
    0% {
      opacity: 1;
    }

    100% {
      opacity: 0;
    }
  }
</style>

<div class="spinner">
  <div />
  <div />
  <div />
  <div />
  <div />
  <div />
  <div />
  <div />
  <div />
  <div />
  <div />
  <div />
</div>
Enter fullscreen mode Exit fullscreen mode

Implement Character component

<script>
  import { relativeTime } from '../lib'

  export let character = {
    id: 0,
    image: '',
    name: '',
    created: '',
    status: '',
    species: '',
    gender: '',
    origin: '',
    location: '',
  }
</script>

<style>
  .character {
    min-width: 300px;
    border-radius: 0.625rem;
    overflow: hidden;
    margin-bottom: 0.625rem;
    box-shadow: rgba(0, 0, 0, 0.16) 0px 2px 2px 0px,
      rgba(0, 0, 0, 0.08) 0px 0px 0px 1px;
  }

  .image-container {
    position: relative;
    width: 100%;
    background: rgb(32, 35, 41) none repeat scroll 0% 0%;
  }

  .image-container img {
    margin: 0;
    opacity: 1;
    transition: opacity 0.5s ease 0s;
  }

  .name-container {
    width: 100%;
    background: rgb(32, 35, 41) none repeat scroll 0% 0%;
    opacity: 0.8;
    position: absolute;
    bottom: 0px;
    padding: 0.625rem;
  }

  .name {
    color: rgb(245, 245, 245);
    font-size: 1.625rem;
    font-weight: 400;
    margin-bottom: 0.5rem;
    font-stretch: expanded;
  }

  .id {
    color: rgb(187, 187, 187);
    margin: 0;
    font-size: 0.875rem;
    letter-spacing: 0.5px;
  }

  .info {
    padding: 1.25rem;
    height: 100%;
    background: rgb(51, 51, 51) none repeat scroll 0% 0%;
  }

  .info > div {
    display: flex;
    justify-content: space-between;
    align-items: center;
    flex-wrap: nowrap;
    padding: 0.75rem 0px 0.375rem;
    border-bottom: 1px solid var(--boder-bottom-color);
  }

  .info > div span {
    font-size: 0.7rem;
    font-weight: 400;
    color: rgb(158, 158, 158);
  }

  .info > div span:nth-child(2) {
    color: var(--orange-color);
  }
</style>

<article class="character">
  <div class="image-container">
    <div>
      <img src={character.image} alt={character.name} />
    </div>
    <div class="name-container">
      <h2 class="name">{character.name}</h2>
      <p class="id">
        id: {character.id} - created {relativeTime(character.created)}
      </p>
    </div>
  </div>
  <div class="info">
    <div>
      <span>STATUS</span>
      <span>{character.status}</span>
    </div>
    <div>
      <span>SPECIES</span>
      <span>{character.species}</span>
    </div>
    <div>
      <span>GENDER</span>
      <span>{character.gender}</span>
    </div>
    <div>
      <span>ORIGIN</span>
      <span>{character.origin}</span>
    </div>
    <div>
      <span>LAST LOCATION</span>
      <span>{character.location}</span>
    </div>
  </div>
</article>
Enter fullscreen mode Exit fullscreen mode

Create utility functions

Create a folter called lib inside src one, then create an index.js file, so let's implement two util functions to make our life easier:

export const transformCharacter = character => {
  const {
    id,
    name,
    status,
    species,
    created,
    image,
    gender,
    origin,
    location,
  } = character
  return {
    id,
    name,
    image,
    status,
    species,
    gender,
    created,
    origin: origin.name,
    location: location.name,
  }
}

export const relativeTime = created => {
  const createdYear = new Date(created).getFullYear()
  const currentYear = new Date().getFullYear()
  const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' })
  return rtf.format(createdYear - currentYear, 'year')
}
Enter fullscreen mode Exit fullscreen mode

Update App component

<script>
  import { onMount } from 'svelte'
  import { transformCharacter } from './lib'
  import Header from './components/Header.svelte'
  import Loader from './components/Loader.svelte'
  import Character from './components/Character.svelte'
  import Footer from './components/Footer.svelte'

  let loading = true
  let characters = []
  let pageInfo = {}
  let options = {
    root: document.getElementById('scrollArea'),
    rootMargin: '0px',
    threshold: 0.5,
  }
  let observer = new IntersectionObserver(handleIntersection, options)

  async function handleIntersection(event) {
    const [entries] = event
    loading = true
    try {
      if (!entries.isIntersecting || !pageInfo.next) {
        loading = false
        return
      }
      const blob = await fetch(pageInfo.next)
      const { results, info } = await blob.json()
      characters = [
        ...characters,
        ...results.map(result => trasformCharacter(result)),
      ]
      pageInfo = info
      loading = false
    } catch (error) {
      loading = false
      console.log(error)
    }
  }

  onMount(async () => {
    try {
      const blob = await fetch('https://rickandmortyapi.com/api/character')
      const { results, info } = await blob.json()
      characters = results.map(result => trasformCharacter(result))
      pageInfo = info
      loading = false
      observer.observe(document.querySelector('footer'))
    } catch (error) {
      loading = false
      console.log(error)
    }
  })
</script>

<style>
  .container {
    min-height: 50vh;
    background-color: var(--text-color);
  }

  .inner {
    max-width: 80em;
    margin: 0 auto;
    padding: 3rem 0;
    display: grid;
    grid-gap: 20px;
    justify-items: center;
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  }

  .loader {
    display: flex;
    align-items: center;
    justify-content: center;
  }
</style>

<Header />
<section class="container" id="scrollArea">
  <div class="inner">
    {#each characters as character (character.id)}
      <Character {character} />
    {/each}
  </div>
  <div class="loader">
    {#if loading}
      <Loader />
    {/if}
  </div>
  <Footer />
</section>

Enter fullscreen mode Exit fullscreen mode

Notes:
Rick and Marty API docs can be found: here
GitHub Repository: here

Happy coding 👋🏽

Discussion (1)

Collapse
johnpangalos profile image
John Pangalos

After trying this I noticed there was a bug casing my next page fetch to run multiple times for the same next page. I solved it by adding if (loading) return; To the beginning of the handleIntersection function.

Also great tutorial, it was really helpful.