loading...
Cover image for Infinite Scroll with Svelte 3 and IntersectionObserver 😍

Infinite Scroll with Svelte 3 and IntersectionObserver 😍

gcdcoder profile image Gustavo Castillo ・5 min read

Introduction

Recently I've been learning Svelte 3, which 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

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>

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>

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>

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>

Create utility funcions

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

export const trasformCharacter = 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')
}

Update App component

<script>
  import { onMount } from 'svelte'
  import { trasformCharacter } 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>

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

Happy coding 👋🏽

Discussion

pic
Editor guide