SvelteKitTutorial Notes
Sections
- Part 1: Welcome to Svelte
-
Introduction[2023-01-14] -
Reactivity[2023-01-14] -
Props[2023-01-14] -
Logic[2023-01-14] -
Events[2023-01-14] -
Bindings[2023-01-15] -
Lifecycle[2023-01-15] -
Stores[2023-01-16]
- Part 2: Introduction to SvelteKit
-
Concepts[2023-01-16] -
Routing[2023-01-16] -
Loading data[2023-01-16] -
Forms[2023-01-17] - API routes
- Errors and redirects
- Page options
- Part 3: Advanced Svelte
- Motion
- Transitions
- Animations
- Actions
- Advanced Bindings
- Classes
- Component composition
- Context API
- Special elements
- Module context
- Debugging
- Next steps
- Part 4: Advanced SvelteKit
- Hooks
- Stores
- Advanced routing
- Advanced loading
- Environment variables
Notes
Part 1: Welcome to Svelte
Introduction
Svelte
is a tool for building web apps via a declarative, component-based paradigm.- A
component
is a self-contained block of code that encapsulatesHTML
,CSS
andJavaScript
that belong together. - [[Shorthand attributes]] ... i.e.
<img src={src} />
becomes<img {src} />
- In Svelte, components are imported within a
<script>
tag
<script>
import Nested from './Nested.svelte'
</script>
<p>This is a paragraph.</p>
<Nested/>
<style>
p {
color: purple;
font-family: 'Comic Sans MS', cursive;
font-size: 2em;
}
</style>
- Component names are always capitalized, to distinguish them from HTML elements.
- To enable parsing HTML strings use
<p>{@html string}</p>
Reactivity
Reactivity
is how Svelte keeps the DOM in sync with application state
<script>
let count = 0;
function increment() {
count += 1
}
</script>
<button on:click={increment}>
Clicked {count}
{count === 1 ? 'time' : 'times'}
</button>
reactive declarations
,$: [expression or value]
are Svelte's version of computed properties. Multiple statements can be encased in a block-scope
<script>
let count = 0;
$: doubled = count * 2;
function handleClick() {
count += 1;
}
</script>
<button on:click={handleClick}>
Clicked {count}
{count === 1 ? 'time' : 'times'}
</button>
<p>{count} doubled is {doubled}</p>
- Note:
Reactivity
is triggered viaassignments
(i.e. mutating methods won't trigger DOM updates)
Props
Props
are declared in components by via theexport
keyword in the<script>
. An optional defaultValue can also be passed to exported props
<script>
export let answer = [defaultValue];
<script>
Prop spreading
is possible via <Component {...object} /> syntax- Within the template portion of a Svelte component,
$$props
can be referenced to access all props that were passed to a component- Avoid do to poor optimization
Logic
If/else if/else
block syntax:
{#if [expression]}
// if conditional html
{:if else [expression]}
// else if conditional html
{:else}
// else conditional html
{/if}
Each
blocks:
<ul>
{#each cats as cat, index}
<li key={cat.id}>#{index + 1} - {cat.name}</li>
{/each}
</ul>
- Of course, it's possible for a list to change order then we must use a
Keyed each
block:
<ul>
{#each cats as cat, index (cat.id)}
<li key={cat.id}>#{index + 1} - {cat.name}</li>
{/each}
</ul>
Await
blocks
{#await promise}
// render something while promise is resolved
{:then response}
// do something with response
{:catch error}
// do something with error
{/await}
- No loading state? No error handling? Then a shorthand version of the above Await block syntax is possible:
{#await promise then response}
// do something with response
{/await}
Events
Event handlers
can be declared on elements by theon:
directive, (e.g.<div on:click={handleClick} />
)Inline handlers
are cool and performant (e.g.<input on:click={e => {name = event.target.value}} />
)- wrapping inline event handlers in quotation marks might improve syntax highlighting
Event modifiers
preventDefault
stopPropagation
passive
nonpassive
capture
once
self
trusted
- Modifiers are chainable.
on:click|preventDefault|stopPropagation
Component events
:
<script>
import {createEventDispatcher} from 'svelte' const dispatch = createEventDispatcher() const handleClick = () =>
dispatch('eventName', event.detail )
</script>
Component events
don't bubble. They must be explicitly forwarded by intermediate components- The shorthand way to do this is by using
event directives
on intermediate components that have no defined value <input on:message />
- will forwardmessage
events to parent component
- The shorthand way to do this is by using
Bindings
- Svelte supports
two-way binding
via thebind:[attribute]={variable}
directive. (e.g.<input bind:value={name} />
) - In the DOM , everything is a string
- For input
type="checkbox"
, the bind attribute ischecked
(e.g.<input type="checkbox bind:checked={checked} />
) - Radio/Checkbox groups make use of the
bind:group={optionsVariable}
directive - All
bind:
directives have a shorthand version (i.e.bind:[attribute]={variable}
can be shortened tobind:attribute
if attributeName === variableName) - Select elements use the
bind:value
directive - A multi-select can be achieved via the
multiple
attribute
Lifecycle
onMount
: use this to run code after the component renders- Use this to fetch data on the client side
- Does not run on SSR
- If
onMount
returns a function it will run when the component is unMounted
onDestroy
: use this to run code after the component is destroyedbeforeUpdate
andafterUpdate
allow for imperative component manipulationtick
batch updates after all pending state updates resolve
Stores
- Svelte has ships with its own application state
store
module
import { writable } from 'svelte/store'
export const count = writable(0)
---
count.subscribe(value => {
component_local_var = value
})
writable
store has the following methods:.subscribe
lets a component tap into store changessubscribe
return anunsubscribe
event that should be called withinonDestroy
lifecycle hook
.set
to set the state value.update
to update the state value
store
values can be references (an auto-subscribed) via$store
(dollar sign referencing)readable
stores limit external writes
import { readable } from 'svelte/store'
export const time = readable(new Date(), function start(set) {
// implementation goes here
const interval = setInterval(() => {
set(new Date())
}, 1000)
return function stop() {
clearInterval(interval)
}
})
- Stores can be
derived
from other stores:
import { readable, derived } from 'svelte/store'
export const time = readable(new Date(), function start(set) {
const interval = setInterval(() => {
set(new Date())
}, 1000)
return function stop() {
clearInterval(interval)
}
})
const start = new Date()
export const elapsed = derived(time, ($time) => {
return ($time - start) / 1000
})
Custom stores
(resembles React useContext pattern)
import { writable } from 'svelte/store'
function createCount() {
const { subscribe, set, update } = writable(0)
return {
subscribe,
increment: () => {},
decrement: () => {},
reset: () => {},
}
}
export const count = createCount()
$count
has the value- Two-way binding (
bind:
) is possible withwritable
stores
Part 2: Introduction to SvelteKit
Concepts
- SvelteKit is an
app framework
(i.e. a meta-framework) - It comes with built-in solutions for:
- Routing
- Server-side rendering (SSR)
- Data fetching
- Service workers
- TypeScript integration
- Singe-page apps (SPAs)
- Library packaging
- Optimized production builds
- Deployment solutions, etc.
- SvelteKit apps are server-rendered by default
- A SvelteKit app can be thought of as two distinct entities working in tandem: the server and the client
- The Server's basic job is to turn a a request into a response
- Client refers to JavaScript that loads in the browser
Routing
- SvelteKit uses file-based routing
- Routes are created under
src/routes
with the+oage.svelte
naming convention
- Routes are created under
+layout.svelte
naming convention can be used to share common ui among child/sibling routesDynamic routes
are achieved via the[paramVar]
directory naming convention
Loading data
- To fetch data (or execute any server-only code prior to loading an adjacent
+page.svelte
) - define a+page.server.js
file- The code in
+page.server.js
will only run on the server - A special
load
function can be declared whose return value will be passed to the adjacent page route as adata
propexport let data
- The
load
function associated with a dynamic page route will be passed aparams
parameter to determine the specific route param that was hit (i.e.export function load( { params } ) { ... }
- The code in
- To share
load
data among children routes uselayout.server.js
(watch out for name conflicts)
Forms
- Client can send data to the server via the
form
element (usingrequest.formData()
on the server and exporting anactions
)
export const actions = {
default: async ({ cookies, request }) => {
const data = await request.formData()
db.createTodo(cookies.get('userid'), data.get('description'))
},
}
- NOTE: Default actions cannot coexist with named actions.
form
element has an optionalaction
attribute (e.gaction="?/create"
)- Hidden form input example
<form method='POST' action='?/delete'>
<input type='hidden' name='id' value={todo.id} />
<button aria-label='Mark as complete'>✔</button>
{todo.description}
</form>
-
fail
function can be used to return error feedback to user and display on page that sent the failed request- Unhandled errors are automatically hidden by SvelteKit from the user
-
form
props will contain thefail
return content and is only ever populated after a form failureexport let form
to gain access to form prop in component
-
import { enhance } from '$app/forms';
and<form use:enhance ... />
to `progressively enhance the native form element- Progressive enhancement of the native form element enables:
- update of the
form
prop - invalidate all data on successful response (e.g. re-run
load
function on server) - navigate to new page on redirect response
- renders nearest error page on failure
- updates page instead of reload on submission (allowing for animations between states)
- update of the
- Progressive enhancement of the native form element enables:
-
use:enhance
also allows forpending states
andoptimistic UI
-
Enhanced creation
<form
method="POST"
action="?/create"
use:enhance={() => {
creating = true;
return async ({ update }) => {
await update();
creating = false;
};
}}
>
- Optimistic deletion
{#each data.todos.filter((todo) => !deleting.includes(todo.id)) as todo (todo.id)}
<li class="todo" in:fly={{ y: 20 }} out:slide>
<form
method="POST"
action="?/delete"
use:enhance={() => {
deleting = [...deleting, todo.id];
return async ({ update }) => {
await update();
deleting = deleting.filter((id) => id !== todo.id);
};
}}
>
<input type="hidden" name="id" value={todo.id} />
<button aria-label="Mark as complete">✔</button>
{todo.description}
</form>
</li>
{/each}
use:enhance
is very customizable- you can cancel() submissions
- handle redirects
- control whether the form is reset
API Routes
Errors and Redirects
Page options