Svelte is a Component Framework like React and Vue, that aims to compile your code at the build step, allowing you to load a single bundle.js on your page to render your app. This means no virtual DOM, no frameworks on top of frameworks, and no framework to load at runtime. These are pretty big selling points and that’s why Svelte has started to gain popularity over the last year, so we’ll look at how to combine it with some of the most widely used JavaScript libraries. To demonstrate this we are going to build simple blog posting app. First we need to setup the starting point of our project and in the end this will be the final look of it.
Setup
This tutorial is not focusing on the backend, I will use Hasura GraphQL engine for this purpose (everything you need to start this tool is described here). We only need Posts table, which will must have a master key (Hasura needs it to generate the delete method) and the table model will look like this:
posts ( id UUID PRIMARY KEY title TEXT text TEXT )
As a starting point we are going to use digit and svelte official template with custom name:
npx degit sveltejs/template svelte-blog cd svelte-blog npm install npm run dev
This is how the file structure of the project looks like:
First we are going to add TypeScript
For that we will have to change building process because ts files have to be compiled to js before svelte can proceed with final bundle. Now lets install all the needed dependencies:
npm install –save-dev typescript node-sass svelte-preprocess rollup-plugin-typescript2 rollup-plugin-postcss rollup-plugin-scss rollup-plugin-serve
We need to change the initial Rollup configuration as changing the input file to ts, adding the svelte plugin preprocessor option (which we will use for ts and scss files) and adding a typescript plugin with compiler to it. Here I also added preprocessor for scss but we’ll look at it later.
/rollup.config.js
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'; import serve from 'rollup-plugin-serve'; import postcss from 'rollup-plugin-postcss'; import typescript from 'rollup-plugin-typescript2'; import typescriptCompiler from 'typescript'; import sveltePreprocessor from 'svelte-preprocess'; const development = process.env.ROLLUP_WATCH; export default { input: 'src/index.ts', output: { sourcemap: true, format: 'iife', name: 'blog', file: 'public/build/index.js' }, plugins: [ svelte({ dev: development, extensions: ['.svelte'], preprocess: sveltePreprocessor(), emitCss: true }), postcss({ extract: true, minimize: true, use: [ ['sass', { includePaths: [ './theme', './node_modules' ] }] ] }), typescript({ typescript: typescriptCompiler, clean: true }), resolve({ browser: true, dedupe: importee => importee === 'svelte' || importee.startsWith('svelte/') }), commonjs({ include: 'node_modules/**' }), development && serve({ contentBase: './public', open: false }), development && livereload({ watch: './public' }), !development && terser({ sourcemap: true }) ], watch: { clearScreen: false } }
Typescript compiler need configuration file to specify the root files and other options. Here I only added where to look for .ts files:
/tsconfig.json
{ "compilerOptions": { "target": "es6", "baseUrl": "./" }, "include": [ "src/**/*" ] }
Typescript compiler also needs to know .svelte components are by adding type definition for them:
/src/types/svelte.d.ts
declare module '*.svelte' { interface ComponentOptions<Props> { target: HTMLElement; anchor?: HTMLElement; props?: Props; hydrate?: boolean; intro?: boolean; } interface Component<Props> { new (options: ComponentOptions<Props>): any; $set: (props: {}) => any; $on: (event: string, callback: (event: CustomEvent) => any) => any; $destroy: () => any; render: (props?: {}) => { html: string; css: { code: string; map?: string }; head?: string; }; } const component: Component<{}>; export default component; }
Next is GraphQL
We will start by first installing all the necessary dependencies:
npm install –seve-dev graphql apollo-boost svelte-apollo
We will add the necessary files to a separate directory, where will be the configuration file for the Apollo Boost (which is a great way to get started quickly) and another file with the HTTP requests. About the Client configuration, the only thing you need is the endpoint for your GraphQL server. In my case it is local instance of Hasura. I also added options to show what happens when making request or throwing an error.
/src/graphql/apollo.ts
import ApolloClient from 'apollo-boost'; import { setClient } from 'svelte-apollo'; const apolloClient = () => { const client = new ApolloClient({ uri: 'http://localhost:8080/v1/graphql', request: fetchOptions => { console.log('fetchOptions', fetchOptions); }, onError: ({ networkError, graphQLErrors, response, operation }) => { console.log('graphQLErrors', graphQLErrors); console.log('networkError', networkError); console.log('response', response); console.log('operation', operation); }, }); setClient(client); }; export default apolloClient;
The queries and mutations shown here are specific to Hasura as they are automatically generated by it and may differ from those of Graphile or Prism:
/src/graphql/posts.ts
import { getClient, query, mutate } from 'svelte-apollo'; import { gql } from 'apollo-boost'; const GET_POST = gql` query { posts { id title text } } `; const ADD_POST = gql` mutation($postEditTitle: String!, $postEditText: String!) { insert_posts(objects: { title: $postEditTitle, text: $postEditText }) { returning { id } } } `; const DELETE_POST = gql` mutation($postId: uuid!) { delete_posts(where: { id: { _eq: $postId } }) { affected_rows } } `; export const getAllPosts = () => query(getClient(), { query: GET_POST }); export const addPost = (client, params) => { return mutate(client, { mutation: ADD_POST, variables: { postEditTitle: params.title, postEditText: params.text, }, }); }; export const deletePost = (client, postId) => { return mutate(client, { mutation: DELETE_POST, variables: { postId, }, }); };
Last is Material-UI
Before showing the final result of the .svelte components we will also add Material-UI, using the adapted version.
npm install --save-dev @smui/button @smui/paper @smui/textfield @smui/card
All material components can be installed individually or as a complete package. Where you want to use a component, you just import the path to the js file.
import Button from '@smui/button';
As this library uses css, the Rollup scss preprocessor will be used here. It will compile all css of the individual components form the node_modules folder and attach the global and theme files as well. The theme file is a must, and is used to easily change the styling of the components.
/theme/_smui-theme.scss
@import '@material/theme/color-palette'; $mdc-theme-primary: #40b3ff; $mdc-theme-secondary: #676778; $mdc-theme-background: #fff; $mdc-theme-surface: #fff; $mdc-theme-error: $material-color-red-900;
Below is the code for one of the svelte components. I used GraphQL in the template and in the event handler. There is a separate styling, which is in scss format. Material-UI Components I used are like any other components in the template.
/src/components/Posts.svelte
<script lang="ts"> import { getClient } from 'svelte-apollo'; import Button, { Label } from '@smui/button'; import Card, { Content, Actions } from '@smui/card'; import { deletePost, getAllPosts } from '../graphql/posts'; let client = getClient(); let posts = getAllPosts(); const handleDeletePost = (e, post) => { e.preventDefault(); deletePost(client, post.id) .then(data => { posts.refetch(); }) .catch(e => { console.error(e); }); }; </script> <style lang="scss" scoped> .posts-container { padding: 20px; background: rgba(0, 110, 180, 0.2); border-radius: 10px; .posts-all { display: flex; justify-content: center; flex-wrap: wrap; } } .pl-5 { padding-left: 5px; } </style> <section class="mt-20 posts-container"> {#await $posts} <h2 class="pl-5">Loading</h2> {:then result} <h2 class="pl-5">Posts</h2> <div class="posts-all"> {#if result.data.posts.length > 0} {#each result.data.posts as post, i} <Card class="posts-card"> <Content> <h3 class="g-header">{post.title}</h3> <p>{post.text}</p> </Content> <Actions> <Button on:click={e => handleDeletePost(e, post)}> <Label>Delete</Label> </Button> </Actions> </Card> {/each} {:else} <h3>No items</h3> {/if} </div> {:catch e} {e} {/await} </section>
Тhis is what the project looks like at the end.
In conclusion
We made a small blog posting application in which we used Svelte as a development framework and TypeScript for expanding JavaScript functionality. The data management is made by GraphQL – search, add and delete and Material-UI to make building and styling a layout faster by using ready-made components and customisable theme. I hope this setup gives some clarity on how Svelte can integrate different libraries. You can see all the code here.