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
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 </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 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')
}
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>
Notes:
Rick and Marty API docs can be found: here
GitHub Repository: here
Happy coding ππ½
Latest comments (0)