How to make a movie recommender: using Svelte as a front-end application

Juan Domingo Ortuzar
Analytics Vidhya
Published in
11 min readDec 12, 2020

--

Svelte (https://svelte.dev/) is a great JavaScript framework for creating reactive web applications. For this tutorial we are going to make a Single Page Application (SPA), in other words, everything the user can interact is in a single page. Since some applications can use more than one page, we will make a empty page so you can know how to make more pages. Big disclaimer time, I’m not a JavaScript developer, so this code might not be the greatest code ever, but it works and that is what matters. Most of what I learned of Svelte I learned from this tutorial. All the code for this tutorial is here, and all the code for this project is here. So lets start by looking at how will the user interact with our application:

User journey

This is what the user will see when the application starts, the idea is that the user will make a list of movies of the style that he or she wants to watch.

When a user adds a movie, when she clicks the recommend button a nice animation that tells the user that its working.

After a couple of seconds, the user will receive a list of movies as recommendations.

And that’s it, nice and simple and all fits inside 1 page. Now lets go make it.

Coding with Svelte

To install Svelte, you should follow the simple tutorial en in the official Svelte site. With that out of the way we will install Materialize CSS library to allow us to create stylish web sites without having to code CSS files.

Lets start by installing the library using NPM.

npm install materialize-css@next

Now, to be able to use Materialize CSS with Svelte we need to bundle it with Rollup. To do that, we first need a plugin, so lets install it:

npm install rollup-plugin-css-only -D

Now, lets go to our rollup.config.js file and you can copy and paste my file over yours, but there are very small changes.

import svelte from 'rollup-plugin-svelte';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import livereload from 'rollup-plugin-livereload';
import { terser } from 'rollup-plugin-terser';
// Tutorial: Here we are importing our css plugin
import css from "rollup-plugin-css-only";
const production = !process.env.ROLLUP_WATCH;function serve() {
let server;

function toExit() {
if (server) server.kill(0);
}
return {
writeBundle() {
if (server) return;
server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], {
stdio: ['ignore', 'inherit', 'inherit'],
shell: true
});
process.on('SIGTERM', toExit);
process.on('exit', toExit);
}
};
}
export default {
input: 'src/main.js',
output: {
sourcemap: true,
format: 'iife',
name: 'app',
file: 'public/build/bundle.js'
},
plugins: [
svelte({
// enable run-time checks when not in production
dev: !production,
// we'll extract any component CSS out into
// a separate file - better for performance
css: css => {
css.write('public/build/bundle.css');
}
}),
//Tutorial: Here we are linking our bundler to our css files.
//These will be saved in the public folder with the name extra.css
css({output: 'public/extra.css'}),
// If you have external dependencies installed from
// npm, you'll most likely need these plugins. In
// some cases you'll need additional configuration -
// consult the documentation for details:
// <https://github.com/rollup/plugins/tree/master/packages/commonjs>
resolve({
browser: true,
dedupe: ['svelte']
}),
commonjs(),
// In dev mode, call `npm run start` once
// the bundle has been generated
!production && serve(),
// Watch the `public` directory and refresh the
// browser on changes when not in production
!production && livereload('public'),
// If we're building for production (npm run build
// instead of npm run dev), minify
production && terser()
],
watch: {
clearScreen: false
}
};

Now in the folder public in the file index.html we have to add our css link.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width,initial-scale=1'>
<title>Svelte app</title> <link rel='icon' type='image/png' href='/favicon.png'>
<!-- Tutorial: the tag below makes sure that we can use Materialize CSS --!>
<link rel="stylesheet" href="extra.css">
<script defer src='/build/bundle.js'></script>
</head>
<body>
</body>
</html>

Finally, we need to import Materialize CSS to our Svelte components. To do this we go to the App.Svelte file in the src folder. Inside the script tags we can add the following lines:

<script>
import "../node_modules/materialize-css/dist/css/materialize.min.css"
import "../node_modules/materialize-css/dist/js/materialize.min.js"

</script>

Now we can get going to creating the rest of our application without having to code all the CSS. This method will work with other CSS libraries like Bootstrap or Tailwind.

Routing in Svelte

Routing is the process of changing pages or routes in your application. Since our application only needs one page, we don have the need for routing. But we will use it anyways, so you can add it to your own applications. Now, Svelte doesn’t come with an official router, but there is a plugin that allows for routing called Svelte Routing. To install it we will use the following command:

npm install --save svelte-routing

How does Svelte works

Svelte, like many JavaScript frameworks, uses the concept of components. This means that every part of the site should be contained in a component and they should be able to communicate between each other. Now, one of the greatest features (in my humble opinion) of Svelte is that it makes the communication between components really simple. Now, the main component in our application is called App.Svelte (all files inside the src that end in .svelte are considered components). Why don't we start by making a navigation bar for our application.

Inside the src folder lets create another one called components, this is where we are going to save our components. Inside the components lets create a file called Navbar.svelte and lets add the following code:

<script>
import {Link} from 'svelte-routing'
</script>
<nav>
<div class="nav-wrapper">
<div class="container">
<Link to="/"><span class="brand-logo">Movie Recommender</span></Link>
<ul id="nav-mobile" class="right hide-on-med-and-down">
<li><Link to="/" class="brand-logo">Home</Link></li>
<li><Link to="/history" class="brand-logo">History</Link></li>
</ul>
</div>
</div>
</nav>

A Svelte component has three parts a script, style and main(doesn't really have a name, but that is how I call it). The script part of a component is where all your JavaScript logic lives for that component and is anything between the <script> tags, for our Navbar component we are only importing a Link component from the svelte-routing library. The style part of a component is where you can write custom CSS for that component, since each component is independent from each other the CSS won't overwrite each other. You can write you own custom CSS between the <style> tags. Anything that is not in a script or style tag is part of the main part, here you write the HTML part of you component. In the Navbar component, we are using the <nav> to define a navigation bar, a couple of div to wrap our bar in Materialize CSS style. The Link tabs are the important ones, because they allow us to use the svelte-router library by telling them where to go, looking at the tag the routes to the History page, we have a route /history so that is where we are going to go when we click on that Link.

Lets create our pages, in a folder named pages inside our src folder, lets make two empty components: Home.svelte and History.svelte. We will fill them out later, but for now in our App.svelte component, lets add our Navbar.

<script>
import "../node_modules/materialize-css/dist/css/materialize.min.css"
import "../node_modules/materialize-css/dist/js/materialize.min.js"
import { Router, Link, Route } from "svelte-routing";
import Navbar from "./components/Navbar.svelte";
import History from "./pages/History.svelte";
import Home from "./pages/Home.svelte"

</script>
<style>
main {
text-align: center;
padding: 1em;
max-width: 240px;
margin: 0 auto;
background-color: #d5274c;
}
h1 {
color: #ff3e00;
text-transform: uppercase;
font-size: 4em;
font-weight: 100;
}
@media (min-width: 640px) {
main {
max-width: none;
}
}
</style>
<Router>
<Navbar/>
<div class="container">
<Route path="/" component={Home} />
<Route path="/history" component={History} />
</div>
</Router>

Lets not worry about the styling of the component, lets focus in the script part. Other than the import of Materialize CSS, we are importing the elements needed to use a Router. Also, we are importing the Home and History components, so we can link the route with the page. Now the Router tags is in charge to assign the Links and Routes so when you change the URL, Svelte will know which component to load. The Route tags are use to link a URL with a component and a Link tag (like the ones used on the Navbar) assigns a text o button a change of URL when clicked. By adding our Navbar inside the Router tag we are telling Svelte that this component has the ability of changing path using the svelte-routing library. You can have multiple Routers inside a component, but only the components inside the Router can use the svelte-routing library.

Why are we using svelte-routing?

We are using svlete-routing because we are using client-side rendering. This means that when you move inside the application is you computer the one in charge of rendering the site and not a server somewhere in the world. This depends mostly in what you are doing, but if you are using a framework like Svelte, it makes a lot of sense to make the frontend of the application a client-side rendering application.

Lets make some recommendations

After a whole lot of set up, lets make the recommendation part of the tutorial. On the Home page, you can copy and paste the code and I will explain the concepts behind this code:

<script>
import Searchbar from '../components/Searchbar.svelte';
import MovieCard from '../components/MovieCard.svelte';
import { onMount } from "svelte";
var movieCards = [];
var movieIds ={};
var loading = false;
var recommendations = [];
onMount(async () => {
const response = await fetch("<http://localhost:8000/autocomplete>")
movieIds = await response.json()

});
function getMovieIds(movieTitle){
var movieIndex = movieIds[movieTitle];
return movieIndex
};

async function postMovieRecommendations(movieIndexes){
loading = true;
var message;

const response = await fetch("<http://localhost:8000/movie/make_recom>",
{
method: 'POST',
body: JSON.stringify(movieIndexes)
})
message = await response.json();
while(message["status"] == "inProgress"){

const response = await fetch("<http://localhost:8000/status/"+message["jobId>"])
message = await response.json()
}
return message["recommendation"]
}

async function makeRecommendations(){
var recom_index = [];
var i;
for(i = 0; i<movieCards.length; i++){
var movIndx = getMovieIds(movieCards[i])
recom_index.push(movIndx)
}
var newMovies = await postMovieRecommendations(recom_index)

for(i=0; i<newMovies.length; i++){
recommendations = [...recommendations, newMovies[i]["title"]]
}
loading = false;
};
function clearRecommendations(){
recommendations = []
movieCards = []
}
</script>
<style>
</style>
<Searchbar bind:movieTitles={movieCards} />
{#if loading == false}
{#if recommendations.length < 1}
{#if movieCards.length < 1}
<div class="container">
<h2>You can add movies to get recommendations</h2>
</div>
{:else}
<button class="btn waves-effect waves-light" type="submit" name="action" on:click={makeRecommendations}>Recommend
</button>
{#each movieCards as card}
<MovieCard movieTitle={card} />
{/each}
{/if}
{:else}
<button class="btn waves-effect waves-dark" type="submit" name="action" on:click={clearRecommendations}>Clear</button>
{#each recommendations as card}
<MovieCard movieTitle={card} />
{/each}
{/if}
{:else}
<div class="container">
<img src="giphy.gif" alt="Making Recommendations">
</div>
{/if}

We start by importing the MovieCard and Searchbar component, they are defined below. We are also importing onMount this is a great function in Svelte, that allows us to run code as soon as the component is rendered in the page, so the component will wait for this to run before appearing. In this case we use it to call our backend (in the endpoint /autocomplete) to get the all the names and ids of movies in our database.

The following functions are functions that call our backend to generate recommendations. The logic is that we have a list of movie ids and a status called loading. Then we POST the list to our /movie/make_recom, the backend will return a jobId and a status, and we set our loading variable to true. We ask our backend if our job is done and while we are waiting we will be showing our gif. Once the job is done, our backend will return with a list of movies ids that we can change back to our movie titles. Now we will be able to switch between a loading state and a recommending stage in the main part of our component. Svelte allows us to have simple logic like if statements or for loops, like the following examples:

{#each recommendations as card}
<MovieCard movieTitle={card} />
{/each}

Here we are creating a MovieCard component for each movie recommended and the closing the loop. We are giving the movie Id to the component as a variable inside the curly brackets.

{#if loading == false}
/// ... code here
{:else}
<div class="container">
<img src="giphy.gif" alt="Making Recommendations">
</div>
{/if}

In here we are checking if we are waiting for a job we are showing a GIF (the GIF can be found here and must be saved in public folder). This will allow us to change state based on a condition. This and more features are explained in more detail in the official Svelte tutorial.

Movie Card Component

In the components folder, create a new file called MovieCard.svelte . This component will receive a movie title and will render a card.

<script>
export let movieTitle;
</script>
<div class="row">
<div class="col s12 m6">
<div class="card blue-grey darken-1">
<div class="card-content white-text">
<span class="card-title">{movieTitle}</span>

</div>
</div>
</div>
</div>

Search bar component

This component is a simple search bar with an autocomplete feature. To be able to use the autocomplete, we need the names of all the movies in the database. Then we initialize the autocomplete and link it to the search bar tags. For more information, you can look at the Materialize CSS documentation.

<script>
import { onMount } from "svelte";
let response = [];
export let movieTitles;
onMount(async () => {
const response = await fetch("<http://localhost:8000/autocomplete>")
var options = { data: await response.json(),
limit: 5};
var elems = document.querySelectorAll(".autocomplete");
var instances = M.Autocomplete.init(elems, options);
});

function addNewMovie(){
var newMovie = document.getElementById("autocomplete-input").value
movieTitles = [...movieTitles, newMovie]
};

</script>
<style></style><div class="container">
<div class="row">
<div class="col s12">
<div class="row">
<div class="input-field col s12">
<input type="text" id="autocomplete-input" class="autocomplete" />
<label for="autocomplete-input">Movies</label>
<button class="btn waves-effect waves-light" type="submit" name="action" on:click={() => addNewMovie()}>Add
<i class="material-icons right">Movie</i>
</button>
</div>
</div>
</div>
</div>
</div>

Conclusion

Now we have a great way to show the model in a fun way that is not a notebook or a text file. Svelte is a great but new framework, this means a couple of things. First, there is no many people using it and some problems you might not find the answer on Stack Overflow. Second, I don’t know if there will be enough adoption from developers. But I do find the ideas behind Svelte just awesome, the simplicity of the framework, the small bundle size and the how quick it is makes me hope that it will become a great place for new JavaScript developers.

--

--