Merge pull request #4 from benmccann/more-pages

This commit is contained in:
swyx
2021-06-17 11:01:20 +08:00
committed by GitHub
64 changed files with 7509 additions and 40 deletions

View File

@@ -1,6 +1,6 @@
# create-svelte
# Svelte Society 2021 Website
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte);
Using SvelteKit!
## Creating a project

View File

@@ -3,6 +3,7 @@
"version": "0.0.1",
"scripts": {
"dev": "svelte-kit dev",
"start": "svelte-kit dev --open",
"build": "svelte-kit build",
"preview": "svelte-kit preview",
"lint": "prettier --check . && eslint --ignore-path .gitignore .",

View File

@@ -161,11 +161,11 @@ h1 {
}
h2 {
font-size: var(--font-600);
font-size: var(--font-500);
}
h3 {
font-size: var(--font-500);
font-size: var(--font-400);
}
/**
@@ -211,13 +211,6 @@ blockquote {
background: rgba(0, 0, 0, 0.99);
}
main {
display: grid;
place-items: center;
padding: var(--space-200) 0;
margin: 0 auto;
}
.heading {
text-align: center;
margin: var(--space-600);

View File

@@ -0,0 +1,68 @@
<script>
export const primary = false;
export let active = false;
</script>
<style>
div {
border: 2px solid #7e7e93;
border-radius: 5px;
background-color: white;
color: #7e7e93;
display: flex;
align-items: center;
padding: 5px 15px;
position: relative;
}
div:hover {
cursor: pointer;
border-color: var(--color) !important;
color: var(--color) !important;
}
.arrow {
margin-left: 25px;
height: 16px;
-webkit-mask: url(/right-arrow.svg) no-repeat center;
mask: url(/right-arrow.svg) no-repeat center;
background-color: #7e7e93;
}
div:hover .arrow {
background-color: var(--color);
}
.arrow.active {
background-color: var(--color-secondary);
}
.popin {
display: none;
position: absolute;
left: calc(100% - 1rem);
top: 1rem;
z-index: 100;
margin: 0;
padding: 0;
border: 2px solid var(--color);
border-radius: 5px;
background: white;
}
div:hover .popin:not(:empty) {
display: block;
}
@media screen and (max-width: 700px) {
.popin {
left: 1rem;
right: 1rem;
top: 100%
}
}
</style>
<div on:click>
<span>
<slot />
</span>
<div class="arrow" class:active />
<span class="popin"><slot name="menu"></slot></span>
</div>

View File

@@ -0,0 +1,111 @@
<script>
import Tag from "../Tag.svelte";
export let active = false;
export let title = "";
export let description = "";
export let image = "";
export let tags = [];
export let stars = 0;
export let addedOn = new Date();
export let url = "";
export let npm = "";
export let repo = "";
let clipboardCopy = false
const copyToClipboard = (text) => {
navigator.permissions.query({name: "clipboard-write"}).then(result => {
if (result.state == "granted" || result.state == "prompt") {
navigator.clipboard.writeText(text)
clipboardCopy = true
setTimeout(() => {
clipboardCopy = false
}, 1000)
}
}).catch(() => alert("Clipboard copy Permission denied"));
}
</script>
<style>
.card {
display: flex;
flex-direction: column;
max-width: var(--width-card);
padding: 14px;
background: #f3f6f9;
border-radius: 5px;
}
.card h3 {
word-break: break-word;
}
h3 a {
text-decoration: none;
}
h3 a:hover {
text-decoration: underline;
}
.active,
.card:hover {
background: #e8f3fe;
}
.card__tags {
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin-bottom: 1rem;
}
.card__bottom {
display: flex;
justify-content: space-between;
}
.card__bottom > * {
white-space: nowrap;
}
.flex-grow {
flex-grow: 1;
}
@media screen and (max-width: 400px) {
.card {
font-size: 0.9rem;
}
.card h3 {
font-size: 24px;
}
}
</style>
<div class="card" class:active id="component-{escape(title)}">
<h3>
<a href={url}>{title}</a>
{#if npm}<Tag click={() => copyToClipboard(`npm install ${npm}`)} variant="copy" title={clipboardCopy ? 'copied!' : `npm install ${npm}`}/>{/if}
</h3>
<p class="flex-grow"><a href="#component-{escape(title)}">#</a> {description}</p>
<div class="card__tags">
{#each tags as tag}
<Tag title={tag} variant='blue' />
{/each}
</div>
<div class="card__bottom">
<div>
{#if stars > 0}
{#if (repo || url).includes('github')}
<img style="display:inline" src="/images/github_logo.svg" alt="github logo" />
{:else if (repo || url).includes('gitlab')}
<img style="display:inline" src="/images/gitlab_logo.svg" alt="gitlab logo" />
<!-- {:else} -->
{/if}
{/if}
</div>
<div>
&#9733;
<code>{stars}</code>
</div>
<!-- commenting out dates just cause it is not very updated yet - all the cards show same date. put back in when we have better scraping -->
<!-- <datetime value={addedOn}>{new Intl.DateTimeFormat('en-Us').format(Date.parse(addedOn))}</datetime> -->
</div>
{#if image}
<img src={image} alt={title} />
{/if}
</div>

View File

@@ -0,0 +1,44 @@
<script>
export let title;
</script>
<style>
h1 {
font-family: Overpass;
font-style: normal;
font-weight: 600;
line-height: 150%;
margin-bottom: 1rem;
}
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 25px;
}
.list {
margin-bottom: 5rem;
}
@media screen and (max-width: 1024px) {
.grid {
max-width: calc(var(--width-card) * 2 + 25px);
grid-template-columns: repeat(2, 1fr);
margin: 0 auto;
}
}
@media screen and (max-width: 700px) {
.grid {
max-width: var(--width-card);
grid-template-columns: 1fr;
margin: 0 auto;
}
}
</style>
<div class="list">
<h1 id="category-{escape(title)}">{title} <a href="#category-{escape(title)}">#</a></h1>
<div class="grid">
<slot />
</div>
</div>

View File

@@ -143,6 +143,9 @@
},
];
let displayIcon = icons.find((e) => e.name === name);
if (!displayIcon) {
throw Error(`Could not find icon with name ${name}`);
}
</script>
<svg
class="svgicon {name}"
@@ -185,8 +188,6 @@
}
svg.docker {
color: #2496ed;
width: 1.39em;
height: 1em;
}
svg.double-checkmark {
color: #84cc16;

View File

@@ -6,7 +6,7 @@
<!--society section-->
<div class="society-wrapper">
<h5 class="title">Societys arround the world</h5>
<h3 class="title">Societies around the world</h3>
{#each societies as society}
{#if society.continent}
<h6 class="continent">{society.continent}</h6>

View File

@@ -0,0 +1,38 @@
<script>
export let title = "";
export let variant;
export let click = undefined
</script>
<style>
div {
/* max-width: 33%; */
padding: 2px 12px;
border-radius: 5px;
font-family: Overpass;
font-style: normal;
font-weight: normal;
font-size: 12px;
line-height: 150%;
text-align: center;
margin-right: 0.5rem;
margin-bottom: 0.5rem;
}
.red {
color: #ff3e01;
background: #ffdbcf;
}
.blue {
color: #158fff;
background: #ddefff;
}
.copy {
color: #ff6f01;
background: #ffe9cf;
cursor: copy;
}
</style>
<div on:click={click} class={variant}>{title}</div>

View File

@@ -0,0 +1,50 @@
<script>
export let nodes;
export let currentPath;
</script>
<style>
ul {
list-style: none;
margin: 0 0 0 1.75rem;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
}
li::before {
content: "•";
color: var(--svelte-grey-light);
display: inline-block;
width: 1em;
margin-left: -1em;
}
li.active::before {
color: var(--svelte-grey);
}
a {
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
li.active a {
font-weight: bold;
}
@media screen and (max-width: 1024px) {
ul {
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
}
}
@media screen and (max-width: 600px) {
ul {
grid-template-columns: 1fr
}
}
</style>
<ul>
{#each nodes.filter((r) => r.meta.layout !== 'recipeCategory') as node}
<li class:active={node.path.includes(node.path)}>
<a href={node.path}>{node.meta.title}</a>
</li>
{/each}
</ul>

View File

@@ -0,0 +1,28 @@
<style>
div {
background-color: rgba(99,177,249, 0.2);
border-left: 4px solid rgb(99,177,249);
border-radius: 5px;
margin: 1rem 0;
padding: .75rem 1.5rem 1rem 1.5rem;
}
div :global(a) {
color: inherit;
border-bottom: 2px dashed rgb(99,177,249);
margin: 0 -3px;
padding: 0 3px;
}
div :global(a:active),
div :global(a:focus),
div :global(a:hover) {
background: rgb(99,177,249);
border-bottom: 2px solid black;
color: black;
text-decoration: none;
}
</style>
<div>
<slot></slot>
</div>

View File

@@ -0,0 +1,28 @@
<style>
div {
background-color: rgba(99,249,105, 0.2);
border-left: 4px solid rgb(99,249,105);
border-radius: 5px;
margin: 1rem 0;
padding: .75rem 1.5rem 1rem 1.5rem;
}
div :global(a) {
color: inherit;
border-bottom: 2px dashed rgb(99,249,105);
margin: 0 -3px;
padding: 0 3px;
}
div :global(a:active),
div :global(a:focus),
div :global(a:hover) {
background: rgb(99,249,105);
border-bottom: 2px solid black;
color: black;
text-decoration: none;
}
</style>
<div>
<slot></slot>
</div>

View File

@@ -0,0 +1,28 @@
<style>
div {
background-color: rgba(255,62,1, 0.2);
border-left: 4px solid rgb(255,62,1);
border-radius: 5px;
margin: 1rem 0;
padding: .75rem 1.5rem 1rem 1.5rem;
}
div :global(a) {
color: inherit;
border-bottom: 2px dashed rgb(255, 62, 1);
margin: 0 -3px;
padding: 0 3px;
}
div :global(a:active),
div :global(a:focus),
div :global(a:hover) {
background: rgba(255, 62, 1);
border-bottom: 2px solid black;
color: black;
text-decoration: none;
}
</style>
<div>
<slot></slot>
</div>

View File

@@ -0,0 +1,74 @@
<script>
import CategoryTree from "$lib/components/recipes/CategoryTree.svelte";
import { categories } from '$lib/stores/recipes';
import { page } from '$app/stores';
export let title,
description = "";
</script>
<main>
<div class="TOC">
<h1>Table of Contents</h1>
{#each $categories as node}
<div class="TOCLink" class:active={$page.path.includes(node.path)}>
<img src={node.meta.icon} alt="" />
<a href={node.path}>{node.meta.title}</a>
</div>
{#if $page.path.includes(node.path)}
<CategoryTree nodes={node.children} />
{/if}
{/each}
</div>
<article>
<h1>{title}</h1>
<slot />
</article>
</main>
<style>
.TOCLink {
align-items: baseline;
display: grid;
grid-template-columns: auto 1fr;
grid-gap: 10px;
padding: 1rem 0;
border-bottom: 1px solid #d0deec;
font-size: 1.1em;
}
.TOCLink.active a {
font-weight: bold;
}
.TOCLink img {
height: 1em;
}
@media (min-width: 1024px) {
main {
display: flex;
}
}
main {
margin: 0 auto;
max-width: var(--width-content);
padding: 2rem 1rem;
}
.TOC {
margin-right: 2rem;
flex: 1;
font-family: Overpass;
line-height: 150%;
}
.TOC :global(a) {
color: #2e2e35;
font-weight: normal;
}
article {
flex: 3;
overflow-x: hidden;
}
@media (min-width: 1024px) {
article {
margin-left: 2rem;
}
}
</style>

View File

@@ -0,0 +1,84 @@
<script>
import CategoryTree from "$lib/components/recipes/CategoryTree.svelte";
import { categories } from '$lib/stores/recipes';
import { page } from '$app/stores';
const childrenNodes = $categories.find(c => c.path === $page.path).children || [];
export let title,
description = "";
</script>
<main>
<div class="TOC">
<h1>Table of Contents</h1>
{#each $categories as node}
<div class="TOCLink" class:active={$page.path.includes(node.path)}>
<img src={node.meta.icon} alt="" />
<a href={node.path}>{node.meta.title}</a>
</div>
{#if $page.path.includes(node.path)}
<CategoryTree nodes={node.children} />
{/if}
{/each}
</div>
<article>
<h1>{title}</h1>
<slot />
<ul>
{#each childrenNodes as node}
<li>
<a href={node.path}>{node.meta.title}</a>
</li>
{/each}
</ul>
</article>
</main>
<style>
.TOCLink {
align-items: baseline;
display: grid;
grid-template-columns: auto 1fr;
grid-gap: 10px;
padding: 1rem 0;
border-bottom: 1px solid #d0deec;
font-size: 1.1em;
}
.TOCLink.active a {
font-weight: bold;
}
.TOCLink img {
height: 1em;
}
@media (min-width: 1024px) {
main {
display: flex;
}
}
main {
margin: 0 auto;
max-width: var(--width-content);
padding: 2rem 1rem;
}
.TOC {
margin-right: 2rem;
flex: 1;
font-family: Overpass;
line-height: 150%;
}
.TOC :global(a) {
color: #2e2e35;
font-weight: normal;
}
article {
flex: 3;
overflow-x: hidden;
}
@media (min-width: 1024px) {
article {
margin-left: 2rem;
}
}
</style>

15
src/lib/stores/recipes.ts Normal file
View File

@@ -0,0 +1,15 @@
import { Writable, writable } from 'svelte/store'
type Recipe = {
meta: any;
filename: string;
path: string;
children: Recipe[]
}
type RecipeStore = {
subscribe: Writable<Recipe[]>["subscribe"],
set: Writable<Recipe[]>["set"]
}
export const categories: RecipeStore = writable([]);

View File

@@ -12,7 +12,7 @@
Want to contribute? Pick up an issue on
<a
class="ghlink"
href="https://github.com/svelte-society/sveltesociety.dev"
href="https://github.com/svelte-society/sveltesociety-2021/"
target="_blank"
rel="noopener">GitHub</a
>!

View File

@@ -23,11 +23,9 @@
>{name}</Link
>
{/each}
</ul>
<div class="logo">
<li class="logo">
<a href="/"> <img alt="Svelte Society Logo" src="/images/logo.svg" /> </a>
</div>
<ul>
</li>
{#each linksRight as [path, name]}
<Link {path} active={path === '/' ? $page.path === '/' : $page.path.includes(path)}
>{name}</Link
@@ -45,18 +43,15 @@
margin-bottom: calc(var(--space-600) * 2);
}
nav {
display: grid;
grid-gap: var(--space-600);
grid-template-columns: auto auto auto;
justify-content: center;
place-items: center;
font-weight: bold;
}
ul {
list-style: none;
padding: 0;
display: grid;
grid-gap: var(--space-600);
grid-template-columns: repeat(3, 1fr) auto repeat(3, 1fr);
justify-content: center;
place-items: center;
font-weight: bold;
}
header {
@@ -64,9 +59,15 @@
}
@media screen and (max-width: 920px) {
nav {
ul {
grid-template-columns: auto;
}
.logo {
position: absolute;
right: 0;
top: 0;
width: 3rem;
}
}
.logo {
display: flex;

View File

@@ -31,9 +31,9 @@
display: grid;
grid-template-rows: 0fr 1fr 0fr;
}
main {
min-height: 100vh;
.container main {
padding-left: 1rem;
padding-bottom: 1rem;
}
</style>

View File

@@ -0,0 +1,22 @@
<svelte:head>
<title>About Svelte Society</title>
</svelte:head>
<div class="wrapper">
<slot />
</div>
<style>
.wrapper {
max-width: 65ch;
}
.wrapper :global(h2), .wrapper :global(h3) {
margin-top: 2rem;
margin-bottom: 1.25rem;
}
.wrapper :global(p) {
margin-bottom: 1.25rem;
}
.wrapper :global(li) {
margin-bottom: 1.1rem;
}
</style>

View File

@@ -0,0 +1,43 @@
# About Svelte Society
Svelte Society is the global Svelte community organization. It started as a series of meetups in [New York](https://www.downtomeet.com/Svelte-Society-NYC), [London](https://www.meetup.com/svelte/), and [Stockholm](https://www.meetup.com/Sveltejs-Society-Stockholm/), then combined into a global network in early 2020. Today it comprises:
- [This website](https://github.com/svelte-society/sveltesociety.dev), which documents Recipes and Examples beyond [the official docs](https://svelte.dev/) and [Events about Svelte](https://sveltesociety.dev/events)
- Podcast: https://www.svelteradio.com/
- Conference: https://sveltesummit.com/
- Discord: https://discord.gg/tnu54nB
- YouTube: http://youtube.com/SvelteSociety
- Twitter: https://twitter.com/sveltesociety
- [~20 meetups across 3 continents](https://twitter.com/SvelteSociety/status/1235264100600631296?s=20)
## Svelte Society Code of Conduct
All interaction on any platform associated with Svelte Society is subject to a Code of Conduct, enforced by Svelte maintainers and Svelte Society organizers. Even though it'd be great if we lived in a world where this wasn't an issue, Code of Conducts are required to ensure that everyone can attend events without reservations.
### TL;DR
Svelte Society is dedicated to providing **a harassment-free community for everyone**, regardless of sex, gender identity or expression, sexual orientation, disability, physical appearance, age, race, nationality, or religious beliefs. **We do not tolerate harassment of community members in any form.** Participants violating these rules may be sanctioned or expelled from the community at the discretion of Svelte Society organizers.
All attendees, speakers, sponsors, instructors, and volunteers at our events are required to agree with the following code of conduct. Leadership will enforce this code at all times. We expect cooperation from all participants to help ensure a safe environment for everybody.
If you have questions or feedback about this Code of Conduct please [contact the organizers](https://discord.gg/tnu54nB).
### No Harassment
Harassment includes offensive verbal or written comments related to sex, gender identity or expression, sexual orientation, disability, physical appearance, age, race, nationality, or religious beliefs, deliberate intimidation, threats, stalking, following, harassing photography or recording, sustained disruption of talks or other events, inappropriate physical contact, and unwelcome sexual attention. Sexual language and imagery is not appropriate for any Svelte Society event or communication channel. Community members asked to stop any harassing behavior are expected to comply immediately.
If a community member engages in harassing behavior, Svelte Society organizers may take any action they deem appropriate, including warning the offender, expulsion from the Svelte Society community, and/or removal from an event. If you are being harassed, notice that someone else is being harassed, or have any other concerns, please contact a Svelte Society organizer immediately, or as soon as you feel comfortable doing so.
### Diversity and Inclusion
Svelte is for everyone. Svelte Society understands that it needs to have representative voices from a range of diverse backgrounds for the Svelte community to thrive. This is reflected in the Svelte Society Discord and Twitter.
However, a lot of work remains to be done. The original Svelte Society meetups and Svelte Society Day France had women co-organizers, and Svelte events have had some speakers from under-represented minorities. However, to date we have yet to have under-represented minorities on the Svelte Society organizer roster. If you are interested in helping us meet this goal, please [get in touch](https://discord.gg/tnu54nB).
### Acknowledgements
Special thanks to [VueMeetups](https://events.vuejs.org/code-of-conduct/#english) for the basis of this Code of Conduct.
## License information
The license information can be found [here](https://sveltesociety.dev/licenses).

View File

@@ -0,0 +1,24 @@
const components = new Array(6)
.fill({
image: "/preview-image.png",
title: "svelte-tooltip",
description:
"A tooltip action that you can attach to any element and pass in a Svelte component...",
tags: [],
stars: 511,
addedOn: new Date(),
category: "forms"
})
.concat(
new Array(3).fill({
image: "/preview-image.png",
title: "svelte-ui",
description:
"A tooltip action that you can attach to any element and pass in a Svelte component...",
tags: [],
stars: 511,
addedOn: new Date(),
category: "ui"
})
);
export default components;

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,212 @@
<script>
import ComponentCard from "$lib/components/ComponentIndex/Card.svelte";
import List from "$lib/components/ComponentIndex/CardList.svelte";
import Button from "$lib/components/ComponentIndex/ArrowButton.svelte";
import components from "./components.json";
let searchValue;
let searchTag;
const tags = Array.from(new Set(components.map(item => item.tags).flat()))
const allCategories = Array.from(new Set(components.map(item => item.category).flat()))
let filterTag = []
let filterCategory = null
let sorting = 'added_desc';
const intersection = (array1, array2) => {
return array1.filter(item => array2.includes(item))
}
$: dataToDisplay = components.filter(component => {
if (!searchValue && filterTag.length === 0 && filterCategory === null) return true
if (
(searchValue && !(component.title.toLowerCase().includes(searchValue.toLowerCase()) || component.description.toLowerCase().includes(searchValue.toLowerCase())))
|| (filterTag.length > 0 && intersection(filterTag, component.tags).length === 0)
|| (filterCategory !== null && component.category !== filterCategory)
) {
return false
}
return true
}).sort((componentA, componentB) => {
switch (sorting) {
case "added_desc": return new Date(componentB.addedOn || '').getTime() - new Date(componentA.addedOn || '').getTime()
case "added_asc": return new Date(componentA.addedOn || '').getTime() - new Date(componentB.addedOn || '').getTime()
case "name_asc": return componentA.title.toLowerCase().localeCompare(componentB.title.toLowerCase())
case "name_desc": return componentB.title.toLowerCase().localeCompare(componentA.title.toLowerCase())
case "stars_desc": return (componentB.stars || 0) - (componentA.stars || 0)
case "stars_asc": return (componentA.stars || 0) - (componentB.stars || 0)
}
});
$: tagSearchResult = searchTag ? tags.filter(item => item.includes(searchTag)) : tags
$: categories = Array.from(new Set(dataToDisplay.map(item => item.category)))
</script>
<style>
.controls {
display: flex;
justify-content: space-between;
align-items: center;
font-family: Overpass;
position: relative;
}
.inputs {
display: grid;
grid-template-columns: repeat(3, auto);
grid-gap: 0.5rem;
margin-right: 2rem;
}
.searchbar {
height: 100%;
width: 35%;
font-family: Overpass;
border-width: 0;
background: #f3f6f9 url(/search-icon.svg) 98% no-repeat;
margin: 0;
padding: 10px 15px;
}
.searchbar-count {
position: absolute;
top: 100%;
right: 0;
}
ul.popin {
padding: 0.5ex;
margin: 0;
font-size: 0.7em;
max-height: 50vh;
overflow-y: auto;
}
ul.popin li {
list-style: none;
padding: 0;
margin: 0;
text-transform: capitalize;
}
ul.popin li:hover {
background: #eee;
border-radius: 3px;
}
ul.popin li.tag-search {
position: sticky;
top: -0.5ex;
margin: 0 -0.5ex;
padding: 0.5ex;
border-radius: 4px;
background: white;
}
ul.popin li.tag-search input {
margin: 0;
background: #f3f6f9;
width: 100%;
min-width: 15ch;
}
ul.popin li.tag-search:hover {
background: white;
}
ul.popin.no-wrap li {
white-space: nowrap;
}
ul.popin li label {
display: flex;
align-items: center;
line-height: 0.7rem;
padding: 0.8ex;
}
ul.popin li input {
flex: 0;
margin: 0 1ex 0 0;
}
@media screen and (max-width: 1024px) {
.controls {
flex-flow: column-reverse;
}
.inputs {
align-self: flex-start;
width: 100%;
grid-template-columns: repeat(3, auto);
}
.searchbar {
align-self: flex-end;
margin-bottom: 1ex;
}
}
@media screen and (max-width: 700px) {
.controls {
align-items: stretch;
}
.inputs {
grid-template-columns: auto;
}
.searchbar {
width: auto;
align-self: stretch;
}
}
</style>
<main class="wrapper">
<h1>Components</h1>
<div class="controls">
<div class="inputs">
<Button active={filterTag.length > 0}>
Tags {#if filterTag.length > 0}<small>({filterTag.length})</small>{/if}
<ul slot="menu" role="menu" class="popin">
<li class="tag-search"><input placeholder="Search for tags..." bind:value={searchTag} /></li>
{#each tagSearchResult as tag}
<li><label><input type="checkbox" bind:group={filterTag} value={tag}> {tag}</label></li>
{/each}
</ul>
</Button>
<Button active={filterCategory !== null}>
Categories
<ul slot="menu" role="menu" class="popin">
<li><label><input type="radio" bind:group={filterCategory} value={null}> All</label></li>
{#each allCategories as category}
<li><label><input type="radio" bind:group={filterCategory} value={category}> {category||"Unclassified"}</label></li>
{/each}
</ul>
</Button>
<Button on:click={() => location.href = "/help/components"}>Submit a component</Button>
<Button active={sorting !== ''}>
Sort
<ul slot="menu" role="menu" class="popin no-wrap">
<li><label><input type="radio" bind:group={sorting} value="added_desc"> Last Added first</label></li>
<li><label><input type="radio" bind:group={sorting} value="added_asc"> Oldest first</label></li>
<li><label><input type="radio" bind:group={sorting} value="stars_desc"> Stars Desc</label></li>
<li><label><input type="radio" bind:group={sorting} value="stars_asc"> Stars Asc</label></li>
<li><label><input type="radio" bind:group={sorting} value="name_asc"> Name Asc</label></li>
<li><label><input type="radio" bind:group={sorting} value="name_desc"> Name Desc</label></li>
</ul>
</Button>
</div>
<input
class="searchbar"
type="text"
placeholder="Search for components..."
bind:value={searchValue} />
<span class="searchbar-count">{dataToDisplay.length} result{#if dataToDisplay.length !== 1}s{/if}</span>
</div>
<hr />
{#each categories as category}
<List title={category||"Unclassified"}>
{#each dataToDisplay.filter(d => d.category === category) as data}
<ComponentCard {...data} />
{/each}
</List>
{/each}
</main>

View File

@@ -34,7 +34,6 @@
<style>
.wrapper {
display: flex;
margin: 2vh 3vw;
}
.event-wrapper {
flex: 1 1 auto;

View File

@@ -0,0 +1,6 @@
<svelte:head>
<title>Help</title>
</svelte:head>
<main>
<slot />
</main>

View File

@@ -0,0 +1,53 @@
<style>
code { background: #eee; }
pre { background: #eee; color: #333; padding: 1rem; border-radius: 1rem }
h2 { font-size: 23px; }
</style>
# How to submit a new component?
To add a new component on the website, the process is rather simple.
You have to add your component in [components.json](https://github.com/svelte-society/sveltesociety.dev/blob/master/src/pages/components/components.json)
## Forking the project
You can fork the [GitHub project](https://github.com/svelte-society/sveltesociety.dev/), add your component and then propose a Pull request.
## Edit the file
You can edit and propose your changes [directly in GitHub](https://github.com/svelte-society/sveltesociety.dev/edit/master/src/pages/components/components.json)
<hr />
# What information should I give ?
Each component is represented by a JSON Object.
The object contains:
- how the component should be named (`title`)
- the url where to found it (`url`)
- a short description of the components (`description`)
- a list of tags (`tags`, avoid creating new tags)
- an image to quickly view what your component is all about (`image`)
- the date when the component have been added on the website (`addedOn`, generally it's today)
- the category of the component (`category`, one of `Boilerplate`, `Data Visualisation`, `Design Pattern`, `Design System`, `Developer Experience`, `Forms & User Input`, `Integration`, `Routers`, `Stores`, `Testing`, `User Interaction`)
- the npm name of the component (`npm`)
_(keys `npm`, `url`, `description`, `image`, `category` are optional)_
Here an example:
```json
{
"title": "svelte-calendar",
"url": "https://github.com/6eDesign/svelte-calendar",
"description": "A lightweight date picker with neat animations and a unique UX",
"tags": [
"components and libraries",
"time and date"
],
"image": "",
"addedOn": "2020-09-29T14:39:13Z",
"category": "Forms & User Input"
}
```

View File

@@ -13,13 +13,13 @@
<div class="wrapper">
<h1>Welcome to Svelte Society!</h1>
<p>
We are a bunch of losely connected groups around the world that strive to push Svelte into the
mainstream. On this page you can find resources such as events, an <a href="/components"
>up-to-date list of components</a
> as well as recipes and other kinds of media.
We are a volunteer global network of Svelte fans that strive to promote Svelte and its ecosystem.
As a service to the community, this site is a central index of <a href="/events">events</a>,
a <a href="/components">components directory</a>, as well as <a href="/receipes">recipes</a> and other useful resources.
Join us or help us out!
</p>
<p>
If you want to reach out to us or just find other like-minded people from around the world you
If you want to reach out to us or find like-minded people from around the world you
can check out the resources below:
</p>
<ul>
@@ -33,6 +33,10 @@
>Twitter</Link
>
</ul>
<p>
We also run the biannual <a href="https://sveltesummit.com/">Svelte Summit</a> conference
and host <a href="https://www.svelteradio.com/">the Svelte Radio podcast</a>.
</p>
</div>
<style>
@@ -44,6 +48,7 @@
margin: 0 auto;
line-height: 1.6;
max-width: 60ch;
margin-bottom: var(--space-300);
}
ul {
display: grid;

View File

@@ -0,0 +1,46 @@
<script lang="ts" context="module">
import { categories } from '$lib/stores/recipes';
export async function load({ fetch }) {
const res = await fetch('/recipes/recipes');
const recipeCategories = await res.json();
if (!res.ok) {
return {
status: res.status,
error: new Error()
};
}
categories.set(recipeCategories);
return { props: { categories: recipeCategories } };
}
</script>
<svelte:head>
<title>Svelte Recipes</title>
<link rel="stylesheet" href="/prism.css" />
</svelte:head>
<div class="container">
<slot />
</div>
<style>
:global(article blockquote) {
background: rgba(255, 62, 1, 0.2);
border-radius: 5px 0px 0px 5px;
color: black;
border-left: 2px solid #ff3e01;
}
.container :global(h2), .container :global(h3) {
margin-top: 2rem;
margin-bottom: 1.25rem;
}
.container :global(p) {
margin-bottom: 1.25rem;
}
.container :global(li) {
margin-bottom: 1.1rem;
}
</style>

View File

@@ -0,0 +1,11 @@
---
title: Build Setup
layout: recipeCategory
icon: "hammer"
---
<!-- routify:options index=1 -->
## Using Svelte with other technologies (e.g. PostCSS, SCSS, TypeScript, and Babel)
Svelte doesn't build in any opinions on the kind of JavaScript, CSS, or even HTML you write. Therefore, if you want to use any nonstandard syntax, you need to put that into your bundler plugin chain or Svelte's preprocessors. Here we will describe the top usecases for doing this.

View File

@@ -0,0 +1,78 @@
---
title: Transpiling ES6 to ES5 for Legacy Browser (IE11) Support with Babel
layout: recipe
---
Svelte outputs modern JavaScript, therefore for legacy browser support, you need to transpile this output back to ES5 (or older). The main strategy people adopt is having 2 builds:
- Modern JS build - transpile Svelte as is
- Legacy browser build - add Babel plugin to bundler, after Svelte plugin.
You can use an environment variable like `process.env.IS_LEGACY_BUILD` (the name is arbitrary - Sapper calls it [`SAPPER_LEGACY_BUILD`](https://github.com/sveltejs/sapper-template/blob/57c430390a5980a9e461206763a619c64ed910e0/rollup.config.js#L12)) to toggle this behavior between builds.
If you are using [Parcel](https://parceljs.org/), this should be taken care of by specifying the correct `.babelrc`.
If you are using [Sapper](https://github.com/sveltejs/sapper-template), this should be correctly set up for you.
If you are using Rollup or Webpack, you need to add the respective Babel plugins.
<details>
<summary>Rollup</summary>
Assuming you want to toggle on and off a modern and a legacy build using a `IS_LEGACY_BUILD` environment variable:
```js
// // rollup.config.js
// other imports
import svelte from "rollup-plugin-svelte";
import babel from "rollup-plugin-babel";
const legacy = !!process.env.IS_LEGACY_BUILD;
export default {
// etc...
plugins: [
svelte({
// etc..
}),
// this should come after the Svelte plugin
legacy &&
babel({
extensions: [".js", ".mjs", ".html", ".svelte"],
runtimeHelpers: true,
exclude: ["node_modules/@babel/**"],
presets: [
[
"@babel/preset-env",
{
targets: "> 0.25%, not dead",
},
],
],
plugins: [
"@babel/plugin-syntax-dynamic-import",
[
"@babel/plugin-transform-runtime",
{
useESModules: true,
},
],
],
}),
// ...
],
};
```
Of course, make sure to have the relevant dependencies like `rollup-plugin-babel @babel/core` and whatever presets and plugins you use installed.
</details>
<details>
<summary>Webpack
</summary>
_To be written - please contribute!_
</details>

View File

@@ -0,0 +1,83 @@
---
title: Using Future JS Syntax in Svelte with Babel
layout: recipe
---
The other, less common but still handy usecase for Babel with Svelte is transpiling _future_ syntax inside `<script>` tags. For example, the [Stage 3 Optional Chaining proposal](https://babeljs.io/docs/en/babel-plugin-proposal-optional-chaining) is popular, but not yet in browsers, and more relevantly, is not yet understood by [Acorn](https://github.com/acornjs/acorn), which is what Svelte uses to parse `<script>` tags.
<details>
<summary>
Example usage:
</summary>
```svelte
<script>
let foo = {
bar: {
baz: true,
},
};
let sub = foo?.ban?.baz;
</script>
<main>
<h1>Hello {sub}!</h1>
</main>
```
When we try to run this, Acorn throws a `ParseError: Unexpected token` error.
</details>
Here, the fix is to use Svelte's [Preprocess](https://svelte.dev/docs#svelte_preprocess) feature:
```js
// rollup.config.js
// ...
export default {
// ...
plugins: [
svelte({
// ...
preprocess: {
script: ({ content }) => {
return require("@babel/core").transform(content, {
plugins: ["@babel/plugin-proposal-class-properties"],
});
},
},
}),
// ...
```
Of course, make sure that `@babel/core` and whatever plugins you use are installed.
Alternatively, you might wish to use [`svelte-preprocess`](https://github.com/kaisermann/svelte-preprocess/) instead, for its' other features:
```js
import preprocess from "svelte-preprocess";
// ...
preprocess: preprocess({
babel: {
presets: [
[
"@babel/preset-env",
{
loose: true,
// No need for babel to resolve modules
modules: false,
targets: {
// ! Very important. Target es6+
esmodules: true,
},
},
],
],
},
});
// ...
```
**Important note: Don't try to transpile to ES5 here**. This would produce unnecessary bloat as you would be transpiling on a per-component basis - [sharing Babel helpers is the reason you use bundler plugins instead](https://github.com/rollup/rollup-plugin-babel#why). Keep it light, only transpile the new stuff you use. This also makes it easy to remove in future.
You may also wish to use new JS syntax inside the JS expressions in your template markup. There is currently no easy way to do that, but [watch this issue](https://github.com/sveltejs/svelte/issues/4701).

View File

@@ -0,0 +1,85 @@
---
title: Using PostCSS with Svelte
layout: recipe
---
**Option 1 - Write your own**
This process is actually [documented in the Svelte docs](https://svelte.dev/docs#svelte_preprocess), but we'll restate it here with the bundler case:
```js
// rollup.config.js
const sass = require('node-sass');
// ...
export default {
// ...
plugins: [
svelte({
// ...
preprocess: {
style: ({ content, attributes, filename }) => {
// only process <style lang="sass">
if (attributes.lang !== 'sass') return;
const { css, stats } = await new Promise((resolve, reject) => sass.render({
file: filename,
data: content,
includePaths: [
dirname(filename),
],
}, (err, result) => {
if (err) reject(err);
else resolve(result);
}));
// TODO: not sure about this. maybe supposed to just return css.toString() instead
return {
code: css.toString(),
dependencies: stats.includedFiles
};
},
},
}),
// ...
```
**Option 2 - use `svelte-preprocess`**
Setting up `svelte-preprocess` can help simplify this:
```js
// rollup.config.js
import svelte from 'rollup-plugin-svelte';
import autoPreprocess from 'svelte-preprocess'
import { scss, coffeescript, pug } from 'svelte-preprocess'
export default {
...,
plugins: [
svelte({
/**
* Auto preprocess supported languages with
* '<template>'/'external src files' support
**/
preprocess: autoPreprocess({ /* options available https://github.com/kaisermann/svelte-preprocess/#user-content-options */ })
})
]
}
```
and you can write SCSS/SASS in your Svelte template:
```svelte
<style lang="scss">
$color: red;
div {
color: $color;
}
</style>
```
The same is true for PostCSS as well, which also applies to any PostCSS plugins, like TailwindCSS (see [Tailwind demo here](https://github.com/tailwindcss/setup-examples/tree/master/examples/sapper)).
## Svelte setup with Tailwind CSS
Here is a specific [guide for adding TailwindCSS to a Svelte app](https://dev.to/swyx/how-to-set-up-svelte-with-tailwind-css-4fg5) (which inclues adding PostCSS).

View File

@@ -0,0 +1,49 @@
---
title: Using TypeScript with Svelte
layout: recipe
---
**It is a common misconception that Svelte doesn't work with TypeScript**. Svelte is, of course, written in TypeScript, so the project strongly believes in the value of typing. An [offical blog post](https://svelte.dev/blog/svelte-and-typescript) announced full support for it in mid 2020. Here is how to get started:
First you need to change you build setup. We recommend using [`svelte-preprocess`](https://github.com/sveltejs/svelte-preprocess/), which will preprocess TypeScript and many more languages out of the box.
Example using `svelte-preprocess`:
```js
// rollup.config.js
import svelte from 'rollup-plugin-svelte';
import autoPreprocess from 'svelte-preprocess'
import typescript from '@rollup/plugin-typescript';
export default {
...,
plugins: [
svelte({
/**
* Auto preprocess supported languages with
* '<template>'/'external src files' support
**/
preprocess: autoPreprocess({ /* options available https://github.com/sveltejs/svelte-preprocess/blob/master/docs/preprocessing.md */ })
}),
/**
* In case you want to use TypeScript outside of Svelte files, too
*/
typescript()
]
}
```
Then you can write your Svelte components with TypeScript:
```svelte
<script lang="ts">
// Compatible with Svelte v3...
export const hello: string = 'world';
</script>
<template>
<div>Hello {hello}</div>
</template>
```
More information from community blogposts:
- https://codechips.me/how-to-use-typescript-with-svelte/

View File

@@ -0,0 +1,293 @@
---
title: Writing Your Own Preprocessors
layout: recipe
---
> This article references `svelte.preprocess` throughout but you may be more familiar with the `preprocess` option of `svelte-loader` or `rollup-plugin-svelte`. This `preprocess` option calls `svelte.preprocess` internally. The bundler plugin gives you easy access to it, so you don't need to transform your components before compilation manually.
The Svelte compiler expects all components it receives to be valid Svelte syntax. To use compile-to-js or compile-to-css languages, you need to make sure that any non-standard syntax is transformed before Svelte tries to parse it. To enable this Svelte provides a `preprocess` method allowing you to transform different parts of the component before it reaches the compiler.
With `svelte.preprocess` you have a great deal of flexibility in how you write your components while ensuring that the Svelte compiler receives a plain component.
### svelte.preprocess
Svelte's `preprocess` method expects an object or an array of objects with one or more of `markup`, `script`, and `style` properties, each being a function receiving the source code as an argument. The preprocessors run in this order.
```js
const preprocess = {
markup,
script,
style,
};
```
In general, preprocessors receive the component source code and must return the transformed source code, either as a string or as an object containing a `code` and `map` property. The `code` property must contain the transformed source code, while the `map` property can optionally contain a sourcemap. The sourcemap is currently unused by Svelte.
## How to make a pre-processor that makes it possible to use Pug/Jade
Pug is an alternative templating language that compiles to html, using whitespace instead of angle brackets:
```js
const pug = require("pug");
const string = "p Hello World!" // pug template language
const html = pug.render(file);
console.log(html); // <p>Hello World!</p>
```
Our goal is to write Pug in our Svelte files instead of HTML where we can do all the scoped styling and JS goodness. Let's create an `app.svelte` file with this:
```svelte
<style>
p {
color: red;
}
</style>
p Hello World!
```
We need to run it through Svelte to generate JavaScript, not HTML:
```js
const svelte = require("svelte/compiler");
const fs = require("fs");
const file = fs.readFileSync("app.svelte", "utf8");
const result = svelte.compile(file);
console.log(result.js.code); // svelte.compile returns both js and sourcemap
```
<details>
<summary>
But this logs out the wrong thing!
</summary>
```js
/* generated by Svelte v3.23.0 */
import {
SvelteComponent,
detach,
init,
insert,
noop,
safe_not_equal,
text
} from "svelte/internal";
function create_fragment(ctx) {
let t;
return {
c() {
t = text("p Hello World!"); // WRONG
},
m(target, anchor) {
insert(target, t, anchor);
},
p: noop,
i: noop,
o: noop,
d(detaching) {
if (detaching) detach(t);
}
};
}
class Component extends SvelteComponent {
constructor(options) {
super();
init(this, options, null, create_fragment, safe_not_equal, {});
}
}
export default Component;
```
</details>
To do this properly, we need to *preprocess* the template file. Let's use `svelte.preprocess` instead of `svelte.compile`:
```js
const result = svelte.preprocess(file, {
markup: ({ content }) => console.log(content)
});
```
Notice also that the `markup` includes the `style` tag as well. Some preprocessors might need that, but we don't, so we can strip it out. Finally, we can run the stripped code through `pug`:
```js
const pug = require("pug");
const svelte = require("svelte/compiler");
const fs = require("fs");
const file = fs.readFileSync("app.svelte", "utf8");
// https://github.com/sveltejs/svelte/blob/8fc85f0ef6b53ed85e54c129d79270fe577626dc/src/compiler/preprocess/index.ts#L97
const styleRegex = /<!--[^]*?-->|<style(\s[^]*?)?>([^]*?)<\/style>/gi;
const scriptRegex = /<!--[^]*?-->|<script(\s[^]*?)?>([^]*?)<\/script>/gi;
(async function () {
const result = await svelte.preprocess(file, {
// options
markup: ({ content }) => {
let code = content.replace(styleRegex, "").replace(scriptRegex, "");
code = pug.render(code);
// TODO: if still want CSS/JS, still have to append the style/script tags BACK onto `code` for svelte to pick them up
return { code };
},
});
console.log(result); // <p>Hello World!</p>
})();
```
This looks right. Our final task is to run this through `svelte.compile`:
```js
const res = svelte.compile(result.code);
console.log(res.js.code);
```
<details>
<summary>
And this generates the correct result.
</summary>
```js
/* generated by Svelte v3.23.0 */
import {
SvelteComponent,
detach,
element,
init,
insert,
noop,
safe_not_equal
} from "svelte/internal";
function create_fragment(ctx) {
let p;
return {
c() {
p = element("p");
p.textContent = "Hello World!"; // correct!
},
m(target, anchor) {
insert(target, p, anchor);
},
p: noop,
i: noop,
o: noop,
d(detaching) {
if (detaching) detach(p);
}
};
}
class Component extends SvelteComponent {
constructor(options) {
super();
init(this, options, null, create_fragment, safe_not_equal, {});
}
}
export default Component;
```
</details>
The [`svelte.preprocess` API](https://svelte.dev/docs#svelte_preprocess) also offers a `dependencies` array you can use to optimize for recompiling when files are changed.
Most of the time, you will be using a bundler plugin rather than using `svelte.compile` and `svelte.preprocess` directly. These plugins have a slightly different API, but you can still slot in your handwritten preprocessor code inline:
```js
// example rollup.config.js
import * as fs from 'fs';
import svelte from 'rollup-plugin-svelte';
const pug = require("pug");
const styleRegex = /<!--[^]*?-->|<style(\s[^]*?)?>([^]*?)<\/style>/gi;
const scriptRegex = /<!--[^]*?-->|<script(\s[^]*?)?>([^]*?)<\/script>/gi;
export default {
input: 'src/main.js',
output: {
file: 'public/bundle.js',
format: 'iife'
},
plugins: [
svelte({
// etc
preprocess: {
markup: ({ content }) => {
let code = content.replace(styleRegex, "").replace(scriptRegex, "");
code = pug.render(code);
return { code };
}
},
})
]
}
```
If you want to use Pug and HTML markup interchangeably, you may wish to adopt the non-standard but community norm of adding a `<template>` tag for non-HTML markup:
```svelte
<style>
p {
color: red;
}
</style>
<template lang="pug">p Hello World!</template>
```
This guarantees no ambiguity when you have a Svelte codebase that is a mix of Pug and HTML templates. In order to process this you will have to add some checking and preprocessing:
```js
const pug = require("pug");
const svelte = require("svelte/compiler");
const fs = require("fs");
const file = fs.readFileSync("app.svelte", "utf8");
// https://github.com/sveltejs/svelte/blob/8fc85f0ef6b53ed85e54c129d79270fe577626dc/src/compiler/preprocess/index.ts#L97
const styleRegex = /<!--[^]*?-->|<style(\s[^]*?)?>([^]*?)<\/style>/gi;
const scriptRegex = /<!--[^]*?-->|<script(\s[^]*?)?>([^]*?)<\/script>/gi;
const markupPattern = new RegExp(
`<template([\\s\\S]*?)(?:>([\\s\\S]*)<\\/template>|/>)`
);
(async function () {
const result = await svelte.preprocess(file, {
// options
markup: ({ content }) => {
// lifted from https://github.com/sveltejs/svelte-preprocess/blob/38b32b110b7e81c995da14dc813f002346b9e0af/src/autoProcess.ts#L179
const templateMatch = content.match(markupPattern);
if (!templateMatch) return { code: content };
const [fullMatch, attributesStr, templateCode] = templateMatch;
/** Transform an attribute string into a key-value object */
const attributes = attributesStr
.split(/\s+/)
.filter(Boolean)
.reduce((acc, attr) => {
const [name, value] = attr.split("=");
// istanbul ignore next
acc[name] = value ? value.replace(/['"]/g, "") : true;
return acc;
}, {});
/** Transform the found template code */
let code = content.replace(styleRegex, "").replace(scriptRegex, "");
if (attributes.lang === "pug") {
code = templateCode;
code = pug.render(code);
}
code =
content.slice(0, templateMatch.index) +
code +
content.slice(templateMatch.index + fullMatch.length);
return { code };
},
});
const res = svelte.compile(result.code);
console.log(res.js.code);
})();
```
This makes the preprocessor only work if `<template lang="pug">` is used.

View File

@@ -0,0 +1,66 @@
---
title: Form Validation with Svelte
layout: recipe
---
### Form Validation with Yup
[Yup](https://github.com/jquense/yup) is a JavaScript schema builder for value passing and validation. We can use Yup to help us validate forms in Svelte.
We can use `bind:value` to bind input value, and validate the form using `schema.validate(values)`.
```svelte
<script>
// Define schema with Yup
import * as yup from 'yup';
const schema = yup.object().shape({
email: yup
.string()
.required('Please provide your email')
.email("Email doesn't look right"),
password: yup.string().required('Password is required'),
});
let values = {};
let errors = {};
async function submitHandler() {
try {
// `abortEarly: false` to get all the errors
await schema.validate(values, { abortEarly: false });
alert(JSON.stringify(values, null, 2));
errors = {};
} catch (err) {
errors = extractErrors(err);
}
}
function extractErrors(err) {
return err.inner.reduce((acc, err) => {
return { ...acc, [err.path]: err.message };
}, {});
}
</script>
<form on:submit|preventDefault={submitHandler}>
<div>
<input
type="text"
name="email"
bind:value={values.email}
placeholder="Your email"
/>
{#if errors.email}{errors.email}{/if}
</div>
<div>
<input
type="password"
name="password"
bind:value={values.password}
placeholder="Password"
/>
{#if errors.password}{errors.password}{/if}
</div>
<div>
<button type="submit">Register</button>
</div>
</form>
```

View File

@@ -0,0 +1,43 @@
---
title: Getting references to Components generated in an each block
layout: recipe
---
Using `bind:this` allows a component to store a reference to it's children, this can also be used when generating a series of components in an `{#each}` block.
**Method 1: Using an array**
This method simply binds the generated component to an array element based on it's index within the loop.
```svelte
<script>
import Child from "./Child.svelte";
const array = [
{ id: 1, title: "apple" },
{ id: 2, title: "banana" },
];
const children = [];
</script>
{#each array as item, i}
<Child title="{item.title}" bind:this="{children[i]}" />
{/each}
```
**Method 2: Using an object as a hashtable**
An alternative is to use an _unique_ key and bind the component to an object, effectively making a hashtable of components.
```svelte
<script>
import Child from "./Child.svelte";
const array = [
{ id: 1, title: "apple" },
{ id: 2, title: "banana" },
];
const children = {};
</script>
{#each array as item, i (item.id)}
<Child title="{item.title}" bind:this="{children[item.id]}" />
{/each}
```

View File

@@ -0,0 +1,9 @@
---
title: Component Recipes
layout: recipeCategory
icon: "code"
---
<!-- routify:options index=1 -->
Some description of this category should go here.

View File

@@ -0,0 +1,19 @@
---
title: Passing attributes to component DOM element
layout: recipe
---
When you want to "forward" any attributes that you can't control before compile time or that changes between each use of the component, like `class` or `style`, to your component wrapper DOM element (instead of declaring variables), use `$$restProps`, like the code below. However, be aware that it isn't generally recommended to use this approach, as it is difficult for Svelte to optimize it, since it doesn't know how many atributes it will receive.
```svelte
<!-- Component.svelte -->
<li {...$$restProps} ><slot></slot></li>
<!-- App.svelte -->
<Component class="li-item-class">{name}</Component>
```
[Svelte Playground here](https://svelte.dev/repl/24139d8599d348b9bcad5c0a1f471230?version=3.23.0). See [relevant part of docs](https://svelte.dev/docs#Attributes_and_props) for more.
This is helpful where, for example, [using MDSveX](https://github.com/pngwn/MDsveX/), you want to create a bunch of Svelte wrappers to DOM elements like `H1`, `P`, and `A` instead of the normal `h1`, `p`, and `a`.
Note that when passing a class to component, you may need to set it to global `:global(.title){...}`

View File

@@ -0,0 +1,235 @@
---
title: Using Fetch to Consume APIs with Svelte
layout: recipe
---
Working with external data in Svelte is important. Here's a guide.
_Maintainers of this Recipe: [swyx](https://twitter.com/swyx)_
### Fetching on Component Mount in Svelte
**Method 1: Using Lifecycles**
We can declare a `data` variable and use the `onMount` lifecycle to fetch on mount and display data in our component:
```svelte
<!-- https://svelte.dev/repl/99c18a89f05d4682baa83cb673135f05?version=3.20.1 -->
<script>
import { onMount } from "svelte";
let data;
onMount(async () => {
data = await fetch(
"https://api.coindesk.com/v1/bpi/currentprice.json"
).then((x) => x.json());
});
</script>
<pre>
{JSON.stringify(data, null, 2)}
</pre>
```
You can further improve this implementation by showing a placeholder while `data` is undefined and also showing an error notification if an error occurs.
**Method 2: Using Await Blocks**
Since it is very common to update your app based on the status of your data fetching, Svelte offers convenient [await blocks](https://svelte.dev/docs#await) to help.
This example is exactly equal to Method 1 above:
```svelte
<!-- https://svelte.dev/repl/977486a651a34eb5bd9167f989ae3e71?version=3.20.1 -->
<script>
let promise = fetch(
"https://api.coindesk.com/v1/bpi/currentprice.json"
).then((x) => x.json());
</script>
{#await promise}
<!-- optionally show something while promise is pending -->
{:then data}
<!-- promise was fulfilled -->
<pre>
{JSON.stringify(data, null, 2)}
</pre
>
{:catch error}
<!-- optionally show something while promise was rejected -->
{/await}
```
Here you can see that it is very intuitive where to place your loading placeholder and error display.
Related Reading:
- https://svelte.dev/docs#2_Assignments_are_reactive
- https://svelte.dev/docs#onMount
- https://svelte.dev/docs#Attributes_and_props
- https://svelte.dev/docs#await
### Fetching on Button Click in Svelte
One flaw with the above approach is that it does not offer a way for the user to refetch data, and additionally we may not want to render on mount (for data saving or UX reasons).
**Method 1: Simple Click Handler**
If we don't want to immediately load data on component mount, we can wait for user interaction instead:
```svelte
<!-- https://svelte.dev/repl/2a8db7627c4744008203ecf12806eb1f?version=3.20.1 -->
<script>
let data;
const handleClick = async () => {
data = await fetch(
"https://api.coindesk.com/v1/bpi/currentprice.json"
).then((x) => x.json());
};
</script>
<button on:click="{handleClick}">
Click to Load Data
</button>
<pre>
{JSON.stringify(data, null, 2)}
</pre>
```
The user now has an intuitive way to refresh their data.
However, there are some problems with this approach. You may still need to declare an extra variable to display error state. More subtly, when the user clicks for a refresh, the stale data still displays on screen, if you are not careful.
**Method 2: Await Blocks**
It would be better to make all these commonplace UI idioms declarative. Await blocks to the rescue again:
```svelte
<!-- https://svelte.dev/repl/98ec1a9a45af4d75ac5bbcb1b5bcb160?version=3.20.1 -->
<script>
let promise;
const handleClick = () => {
promise = fetch(
"https://api.coindesk.com/v1/bpi/currentprice.json"
).then((x) => x.json());
};
</script>
<button on:click="{handleClick}">
Click to Load Data
</button>
{#await promise}
<!-- optionally show something while promise is pending -->
{:then data}
<!-- promise was fulfilled -->
<pre>
{JSON.stringify(data, null, 2)}
</pre
>
{:catch error}
<!-- optionally show something while promise was rejected -->
{/await}
```
The trick here is we can simply reassign the `promise` to trigger a refetch, which then also clears the UI of stale data while fetching.
**Method 3: Promise Swapping**
Of course, it is up to you what UX you want - you may wish to keep displaying stale data and merely display a loading indicator instead while fetching the new data. Here's a possible solution using a second promise to execute the data fetching while the main promise stays onscreen:
```svelte
<!-- https://svelte.dev/repl/21e932515ab24a6fb7ab6d411cce2799?version=3.20.1 -->
<script>
let promise1, promise2;
const handleClick = () => {
promise2 = new Promise((res) =>
setTimeout(() => res(Math.random()), 1000)
).then((x) => {
promise1 = promise2;
return x;
});
};
</script>
<button on:click="{handleClick}">
Click to Load Data {#await promise2}🌀{/await}
</button>
{#await promise1}
<!-- optionally show something while promise is pending -->
{:then value}
<!-- promise was fulfilled -->
<pre>
{value}
</pre
>
{:catch error}
<!-- optionally show something while promise was rejected -->
{/await}
```
**Method 4: Data Stores**
One small flaw with our examples so far is that Svelte will still try to update components that unmount while a promise is still inflight. This is a memory leak that sometimes causes bugs and ugly errors in the console. We should ideally try to cancel our promise if it is unmounted, but of course promise cancellation isn't common. When a component unmounts, Svelte cancels its reactive subscriptions, but an unmounted component has [some other issues that Svelte doesn't clean up](https://github.com/svelte-society/recipes-mvp/issues/6):
- it can still dispatch events
- it can still call callbacks
- other code queued to run after a promise fulfills will still run, possibly causing unwanted side effects
It can be simpler to keep promises out of components, and only put async logic in [Svelte Stores](https://svelte.dev/docs#svelte_store), where you read values and trigger custom methods to update values.
```js
// https://svelte.dev/repl/483ce4b0743f41238584076baadb9fe7?version=3.20.1
// store.js
import { writable } from "svelte/store";
export const count = writable(0);
export const isFetching = writable(false);
export function getNewCount() {
isFetching.set(true);
return new Promise((res) =>
setTimeout(() => {
res(count.set(Math.random()));
isFetching.set(false);
}, 1000)
);
}
```
```svelte
<script>
import { getNewCount, count, isFetching } from "./store";
</script>
<button on:click="{getNewCount}">
Click to Load Data {#if $isFetching}🌀{/if}
</button>
<pre>
{$count}
</pre>
```
This has the added benefit of keeping state around if the component gets remounted again with no need for a new data fetch.
### Dealing with CORS Errors in Svelte
Svelte is purely a frontend framework, so it will be subject to the same CORS restrictions that any frontend framework faces. You will run into CORS issues in two ways:
1. In local development (making requests from `http://localhost` to `https://myapi.com`)
2. In production (making requests from `https://mydomain.com` to `https://theirapi.com`)
You can solve both with a range of solutions from having a local API dev server or proxying requests through a serverless function or API gateway. None are responsibilities of Svelte but here are some helpful resources that may help:
- https://alligator.io/nodejs/solve-cors-once-and-for-all-netlify-dev/
- https://zeit.co/docs/v2/serverless-functions/introduction
- https://docs.begin.com/en/http-functions/api-reference
- https://aws.amazon.com/blogs/mobile/amplify-framework-local-mocking/
If you happen to be running [a Sapper app](https://sapper.svelte.dev/), then you may take advantage of preloading data server-side in Sapper: https://sapper.svelte.dev/docs#Preloading.
### Further Links
- Svelte Suspense discussion: https://github.com/sveltejs/svelte/issues/1736
- Your link here?

View File

@@ -0,0 +1,97 @@
---
title: Authentication with Svelte
layout: recipe
---
Figuring out how to authenticate with Svelte can be tricky business. The official docs for Sapper, the Server-Side Rendering platform designed for Svelte, recognize that session management should be handled by some other service such as [express-session](https://github.com/expressjs/session), but you are not limited to using any backend with Svelte. Moreover, Sapper does not have native support for persistent sessions (as of April 2020).
Your best options might be to offload session management from Svelte to some other web server that is configured to use HTTPS. In many cases, your clients will want to authenticate using a modern web browser, and most modern web browsers implement strong security regulations over how data gets transferred between a server and a client. During development, you might see this error: ["Reason: CORS header 'Access-Control-Allow-Origin' missing"](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors/CORSMissingAllowOrigin), and if you do then it may be worth a few minutes to read up on ["Dealing with CORS Errors in Svelte"](https://github.com/svelte-society/recipes-mvp#dealing-with-cors-errors-in-svelte).
**Method 1: JSON Fetch using a POST method (same-origin cors headers)**
While Svelte may not necessary require an asynchronous authentication method, your application's performance could benefit from trying to use one. It is generally accepted that `POST` methods are the way to go, since they do not append sensitive data after the request URI. In this example, we incorporate writable stores (for saving the auth server's response), reactive statements for building the data body of the POST request, and specialized Svelte tags `{#await <promise>}`, `{:then <awaited response>}`, `{:catch <some error>}` to render a different HTML tag at each stage of the authentication request.
It is important to note that this example includes `preventDefault` to prevent the runtime from making an HTTP request at the instant when the form element gets created: `<form on:submit|preventDefault={submitHandler}>`.
```svelte
<script>
import { session } from './session.js';
/* session.js:
* ===========
* import { writable } from 'svelte/store';
* export const session = writable({ data: "" })
*/
// Alternatively, you could write:
// export const AUTH_SERVER_URL;
// instead of:
// const AUTH_SERVER_URL = "...";
// to set the destination using the props spread
const AUTH_SERVER_URL = "https://authserverurl.com/api/login";
let email = "";
let password = "";
let combined;
let data;
$: combined = {email: email, password: password};
$: if(data) {
$session.data = data;
}
async function authenticate() {
// Gathered from: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
let response = await fetch(AUTH_SERVER_URL, {
method: 'post',
mode: 'cors', // no-cors, *cors, same-origin,
cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json'
// 'Content-Type': 'application/x-www-form-urlencoded',
},
redirect: 'follow', // manual, *follow, error
referrerPolicy: 'no-referrer', // no-referrer, *client
body: JSON.stringify(combined) // body data type must match "Content-Type" header
});
// One additional option is to use:
// ... = await response.json();
// But since we're printing out the response in an HTML element,
// it is convenient to await the `.text()` promise.
let text = await response.text();
// This next line is verbose, but it's meant to demonstrate
// what happens when we want to use a reactive value change
// to bind our new information using `$: if(data) {...}`
let data = text;
return text;
}
function submitHandler() {
/* This promise needs to be awaited somewhere --
* either in the HTML body via `{#await}` tags,
* in a `<script>` tag, or in an imported `.js` module.
*/
result = authenticate();
// Clear out the data fields
email = "";
password = "";
}
</script>
<div>
<form on:submit|preventDefault={submitHandler}>
<input type="text" bind:value={email}>
<input type="password" bind:value={password}>
<button>Submit</button>
</form>
</div>
<div>
{#if result===undefined}
<p />
{:else}
{#await result}
<div><span>Logging in...</span></div>
{:then value}
<div><span>{value}</span></div>
{:catch error}
<div><span>{error.message}</span></div>
{/await}
{/if}
</div>
```

View File

@@ -0,0 +1,9 @@
---
title: Design Patterns
layout: recipeCategory
icon: "palette"
---
<!-- routify:options index=1 -->
Some description of this category should go here.

View File

@@ -0,0 +1,16 @@
---
title: Routing with Svelte
layout: recipe
---
_to be written_
- A map for an app: what is routing
- Different approaches
- XML-stylee: [svelte-routing](https://github.com/EmilTholin/svelte-routing)
- Express-stylee: [navaid](https://github.com/lukeed/navaid)
- FS based routing: [routify](https://routify.dev/)
### Further Links
- https://routify.dev/ (with https://github.com/sveltech/routify-starter)
- https://github.com/EmilTholin/svelte-routing

View File

@@ -0,0 +1,12 @@
---
title: Server-side Rendering
layout: recipe
---
_some content_
- What is it
- What does the API look like
- building an SSR component
- hydrating an SSR component with a client build
- building a simple express-based SSR server thing
- putting it all together

View File

@@ -0,0 +1,180 @@
<script>
import CategoryTree from "$lib/components/recipes/CategoryTree.svelte";
import Icon from "$lib/components/Icon/index.svelte";
import { page } from '$app/stores';
import { categories } from '$lib/stores/recipes';
</script>
<div class="content-wrap">
<div class="my-1">
<h1>Cookbook</h1>
<p class="intro">
This cookbook serves shows users how best-in-practice code is written in
Svelte. Youll learn how to import third-party libraries, external scripts
as well as how to handle common problems that you will have to solve
often.
</p>
</div>
</div>
<section class="recipes-block">
<div class="navigation-block my-1">
<div class="navigation-content">
<h3>Pick a Category to Get Started</h3>
<div class="categories-wrap">
{#each $categories as category}
{#if category}
<div class="category-style">
<div class="list-meta">
<div class="icon-circle">
<Icon
name={category.meta.icon}
width="1.5em"
height="1.5em"
/>
</div>
<a href={category.path} class="list-title"
>{category.meta.title}</a
>
</div>
<CategoryTree currentPath={page.path} nodes={category.children} />
</div>
{/if}
{/each}
</div>
</div>
</div>
</section>
<div class="content-wrap">
<div class="my-1">
<h3>What can I expect from these recipes?</h3>
<p>
The Svelte compiler expects all components it receives to be valid Svelte
syntax. To use compile-to-js or compile-to-css languages, you need to make
sure that any non-standard syntax is transformed before Svelte tries to
parse it. To enable this Svelte provides a preprocess method allowing you
to transform different parts of the component before it reaches the
compiler. With <b>svelte.preprocess</b> you have a great deal of flexibility
in how you write your components while ensuring that the Svelte compiler receives
a plain component.
</p>
</div>
<div class="signup-block my-1">
<h3>Do you want to write a recipe?</h3>
<p>
Were looking for new recipes and recipe authors. Are you interested? Just
submit a issue with a recipe below!
</p>
<a
href="https://github.com/svelte-society/sveltesociety.dev/issues/new"
class="button">Submit</a
>
</div>
<div class="my-1">
<h3>Where to get started</h3>
<p>
If you want the quickest way to get started, clone <a
href="https://github.com/sveltejs/template"
>the Official Svelte App template</a
>. If you want a custom setup, head to the
<a href="/recipes/build-setup">Build Setup recipes</a>.
</p>
<p>
If you are writing a Svelte component library, check the <a
href="https://github.com/sveltejs/component-template"
>the Official Svelte Component template</a
>.
</p>
</div>
</div>
<style>
.icon-circle {
border: 2px solid black;
border-radius: 50%;
height: 2em;
width: 2em;
display: grid;
place-items: center;
padding-left: 4px;
}
.content-wrap {
align-items: center;
width: 65ch;
margin: 0 auto;
}
.navigation-block {
padding: 1rem;
}
.navigation-content h3 {
display:flex;
justify-content: center;
}
.categories-wrap {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
flex-wrap: wrap;
padding: 1rem;
}
@media screen and (max-width: 920px) {
.categories-wrap {
grid-template-columns: auto;
}
}
.category-style {
padding: 1rem;
}
@media screen and (max-width: 1200px) {
.category-style {
width: 100%;
}
.content-wrap,
.content-wrap {
width: inherit;
padding: 0 1rem;
}
}
.signup-block {
background: #ecf6ff;
padding: 1rem;
}
a.button {
text-decoration: none;
display: block;
margin: 0;
border-radius: var(--border-radius);
display: inline-block;
font-size: medium;
font-weight: bold;
margin: 1.5rem 0 0.5rem 0;
padding: 1rem 2rem;
background-color: var(--color-secondary);
border: 2px solid var(--color-secondary);
color: var(--color-bg);
}
a.button:hover {
cursor: pointer;
filter: brightness(var(--hover-brightness));
}
.recipes-block {
background: #f3f6f9;
}
.list-meta {
align-items: center;
display: grid;
grid-template-columns: 50px auto;
}
.list-title {
color: var(--svelte-grey);
margin-left: 1rem;
text-decoration: none;
text-transform: uppercase;
font-family: Georgia, 'Times New Roman', Times, serif
}
.list-title:hover {
text-decoration: underline;
}
</style>

View File

@@ -0,0 +1,104 @@
---
title: Editable SVG Icon Systems with Svelte and Heroicons
layout: recipe
---
There are many ways to create an SVG Icon System, but one method that takes advantage of Svelte's capabilities is to create editable inline icons as components. Some of the advantages of this way of working is:
- They are easy to edit on the fly
- You can use standard props and defaults to keep them to a typical size or alter them if you need to
- They are inline, so no HTTP requests are necessary
- They can be made accessible dynamically
### Base Setup
First, create a folder for all of the icons. Be sure to name them according to some standardized fashion in order to retrieve them easily. For example:
- **components/icons/Pencil.svelte**
- **components/icons/PencilAlt.svelte**
For more a more detailed example of the entire setup, check out the example repo: https://github.com/babycourageous/svelte-recipes-svg-icons-demo or the Demo Site (https://svelte-recipes-svg-icons-demo.netlify.app/)
The next step is to create a base icon component (`Icon.svelte`). Start with the markup which will make use of a `slot`. This will allow the `svg` parent to accept children (which will be the paths of the icon).
```svelte
<svg
xmlns="http://www.w3.org/2000/svg"
width={width}
height={height}
viewBox="0 0 20 20"
aria-labelledby={name}
role="presentation"
>
<title id={name} lang="en">{`${name} icon`}</title>
<g stroke={color} fill={color}>
<slot />
</g>
</svg>
```
You can use this `Icon` component as is - the only thing you might need to update is the `viewBox` depending on the `viewBox` of the icons you are using. The `width`, `height`, `color`, and `name` props of the icon will allow the icon to be dynamically updated.
The `width`, `height`, and `color` props will have defaults so that the icon will be rendered consistently unless other values are explicitly passed in. The `name` prop is used for both the `<title>` content and its `id` for accessibility.
Our script will look like this:
```svelte
<script>
export let width = 16
export let height = 16
export let name = ''
export let color = 'currentColor'
</script>
```
The default `color` prop is set to `currentColor` so the icon will inherit the color of whatever text surrounds it. Of course, this csn be overridden with a specific color.
### Usage
To use it, say we had a Svelte component called `PencilAlt.svelte` that contained only our icon svg paths like so:
```svelte
<path
d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"
/>
<path
fill-rule="evenodd"
d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"
clip-rule="evenodd"
/>
```
We can use the `Icon` component as follows:
```svelte
<Icon name="pencil-alt"><PencilAlt /></Icon>
```
Now, if wed like to make many sizes for the icon, we can do so very easily:
```svelte
<p>
<!-- you can pass in a smaller width and height -->
<Icon
width="12"
height="12"
name="pencilAlt"
><PencilAlt /></Icon>
<!-- or you can use the default, which is 16 -->
<Icon name="pencilAlt"><PencilAlt /></Icon>
<!-- or bump up the size, too -->
<Icon
width="30"
height="30"
name="pencilAlt"
><PencilAlt /></Icon>
</p>
```
### Additional Notes
Some situations require you to recreate or edit every SVG to make global changes. This method can save you that time and hassle. By keeping the logic for the entire icon system in one base component, quick updates can be made and all of your icons will reflect the changes.
### When To Avoid This Pattern
This type of SVG icon system is really useful when you have a number of icons that are used in different ways throughout your site. If youre repeating the same icon many times on one page (e.g. a giant table with a delete icon in each row), it might make more sense to have all of the sprites compiled into a sprite sheet and use <use> tags to load them.

View File

@@ -0,0 +1,9 @@
---
title: Other
layout: recipeCategory
icon: "archive"
---
<!-- routify:options index=1 -->
Some description of this category should go here.

View File

@@ -0,0 +1,97 @@
---
title: Dockerize a Sapper App
layout: recipe
---
_This example is taken from the [https://svelte.dev/](https://svelte.dev/)._
To serve a Sapper app from a Docker container, you can put all the build assets from `__sapper__/build` into a Docker container and run `node __sapper__/build`
Let's pull down the [basic sapper template](https://github.com/sveltejs/sapper-template) using [degit](https://github.com/Rich-Harris/degit).
```
npx degit sveltejs/sapper-template#rollup sapper-docker
cd sapper-docker
```
Next, we need to create a `Dockerfile` in the root of our project.
```dockerfile
FROM mhart/alpine-node:12
WORKDIR /app
COPY package.json .
COPY package-lock.json .
RUN npm ci --prod
COPY static static
COPY __sapper__ __sapper__
EXPOSE 3000
CMD ["node", "__sapper__/build"]
```
You can now build the docker image.
```sh
npm install
npm run build
docker build . -t sapper-docker
```
To speed up the docker build, you can add a `.dockerignore` file in the root of `sapper-docker` folder:
```
node_modules/
```
This is to tell Docker to ignore files / folder in the Docker build context.
Now, you can run your docker image.
```
docker run -p 5000:3000 sapper-docker
```
Open up your browser at localhost:5000 and you should see your sapper app running!
### Building Sapper App in a Docker image
If you want to build your Sapper App in a Docker image instead in your machine, you can do it in an intermediate Docker image, that way, it would not contribute to the final Docker image size.
In that case, you can use a `slim` version of the `alpine-node` Docker image, which comes without `npm` or `yarn` installed
```dockerfile
# build the sapper app
FROM mhart/alpine-node:12 AS build
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
# install dependencies
FROM mhart/alpine-node:12 AS deps
WORKDIR /app
COPY package.json .
COPY package-lock.json .
RUN npm ci --prod
COPY static static
COPY __sapper__ __sapper__
# copy node_modules/ and other build files over
FROM mhart/alpine-node:slim-12
WORKDIR /app
COPY --from=deps /app .
EXPOSE 3000
CMD ["node", "__sapper__/build"]
```

View File

@@ -0,0 +1,57 @@
---
title: Dockerize a Svelte App
layout: recipe
---
Let's pull down the [basic svelte template](https://github.com/sveltejs/template) using [degit](https://github.com/Rich-Harris/degit).
```
npx degit sveltejs/template svelte-docker
cd svelte-docker
```
Run `npm install` to generate the `package-lock.json` file.
### Building
Next, we need to create a file named `Dockerfile` in the root of our project, and paste in the following:
```dockerfile
FROM node:12 AS build
WORKDIR /app
COPY package.json ./
COPY package-lock.json ./
RUN npm install
COPY . ./
RUN npm run build
FROM nginx:1.19-alpine
COPY --from=build /app/public /usr/share/nginx/html
```
This tells Docker to build the Svelte App using an intermediate Node image and then copies only the build output to the final NGINX image, as
we only need something that serves static content. That way, Node doesn't contribute to the final size of the Docker image.
To ensure we are not copying unnecessary files and folders in the build process (the `COPY . ./` step), we can add the following to a file named `.dockerignore`:
```
node_modules
```
You can now build your docker image:
```
docker build . -t svelte-docker
```
### Running
And to run your image:
```
docker run --rm --name=svelte-docker -p 5000:80 svelte-docker
```
Open up your browser at `http://localhost:5000` and your dockerized Svelte App should now be up and running!

View File

@@ -0,0 +1,9 @@
---
title: Publishing and Deploying
layout: recipeCategory
icon: "docker"
---
<!-- routify:options index=1 -->
Some description of this category should go here.

View File

@@ -0,0 +1,38 @@
/**
* @type {import('@sveltejs/kit').RequestHandler}
*/
export async function get() {
const pages = await Promise.all(
Object.entries(import.meta.glob('./**/*.svx'))
.map(async ([path, page]) => {
const { metadata } = await page();
const filename = path.split('/').pop();
path = '/recipes' + path.substring(1, path.length - '.svx'.length);
if (path.endsWith('/index')) {
path = path.substring(0, path.length - '/index'.length);
}
return { meta: metadata, filename, path };
})
);
const categories = pages.filter(page => page.meta.layout === 'recipeCategory');
categories.forEach(category => {
category.children = [];
pages.forEach(p => {
if (category !== p && p.path.startsWith(category.path)) {
category.children.push(p);
}
});
});
if (categories) {
return {
body: categories
};
}
return {
error: new Error(),
body: undefined
};
}

View File

@@ -0,0 +1,226 @@
---
title: Stores
layout: recipeCategory
icon: database
---
<!-- routify:options index=1 -->
<script>
import Warning from '../../../lib/components/recipes/Warning.svelte'
</script>
## This needs to be re-organised into separate recipes
<Warning>
This guide assumes you understand the basics of Svelte Stores. If you aren't familiar with them then working through the <a href="http://www.svelte.dev/tutorial/writable-stores" target="_blank">relevant tutorial</a> and reading the <a href="http://www.svelte.dev/docs#svelte_store" target="_blank">store documentation</a> are highly recommended.
</Warning>
Svelte stores offer a simple mechanism to handle shared state in your Svelte application but looking beyond the built-in store implementations will unlock a whole world of power that you could never have dreamed of. In this episode of _The Tinest Kitchen_ we'll take a close look at [The Store Contract](#The_Store_Contract), learn how to implement [Custom Stores](#Custom_Stores), by making use of the built-in store API, and explore how we can implement [a completely custom store]() without using the built-in stores at all.
### The store contract
The built-in Svelte stores (`readable`, `writable`, and `derived`) are just store _implementations_ and while they are perfectly capable of handling many tasks, sometimes you need something more specific. Although often overlooked, the store _contract_ is what gives these stores their power and flexibility. Without this contract, svelte stores would be awkward to use and require significant amounts of boilerplate.
Svelte does not compile your javascript files and, as such, only observes the store contract inside Svelte components.
#### `store.subscribe`
At its simplest, the store contract is this: any time Svelte sees a variable prepended with `$` in a Svelte component (such as `$store`) it calls the `subscribe` method of that variable. The `subscribe` method must take a single argument, which is a function, and it must _return_ a function that allows any subscribers to unsubscribe when necessary. Whenever the callback function is called, it must be passed the current store value as an argument. The callback passed to subscribe should be called when subscribing and anytime the store value changes.
The following examples aren't the _exact_ code that Svelte produces, rather, simplified examples to illustrate the behaviour.
This:
```js
import { my_store } from "./store.js";
console.log($my_store);
```
Becomes something like this:
```js
import { my_store } from "./store.js";
let $my_store;
const unsubscribe = my_store.subscribe((value) => ($my_store = value));
onDestroy(unsubscribe);
console.log($my_store);
```
The callback function passed to `my_store.subscribe` is called immediately and whenever the store value changes. Here, Svelte has automatically produced some code to assign the `my_store` value to `$my_store` whenever it is called. If `$my_store` is referenced in the component, it also causes those parts of the component to update when the store value changes. When the component is destroyed, Svelte calls the unsubscribe function returned from `my_store.subscribe`.
#### `store.set`
Optionally, a store can have a `set` method. Whenever there is an assignment to a variable prepended with `$` in a Svelte component it calls the `set` method of that variable with newly mutated or reassigned `$variable` as an argument. Typically, this `set` argument should update the store value and call all subscribers, but this is not required. For example, Svelte's `tweened` and `spring` stores do not immediately update their values but rather schedule updates on every frame for as long as the animation lasts. If you decide to take this approach with `set`, we advise not [binding](tutorial/store-bindings) to these stores as the behaviour could be unpredictable.
This:
```js
$my_store = "Hello";
```
Will become something like:
```js
$my_store = "Hello";
my_store.set($my_store);
```
The same is true when assigning to a nested property of a store.
This:
```js
$my_store.greeting = "Hello";
```
Becomes:
```js
$my_store.greeting = "Hello";
my_store.set($my_store);
```
Although Svelte's built-in stores also have an `update` method, this is not part of the contract and is not required to benefit from the automatic subscriptions, unsubscriptions, and updates that the store contract provides. Stores can have as many additional methods as you like, allowing you to build powerful abstractions that take advantage of the automatic reactivity and cleanup that the store contract provides.
To summarise, the store contract states that svelte stores must be an object containing the following methods:
- `subscribe` - Automatically called whenever svelte sees a `$` prepended variable (like `$store`) ensuring that the `$` prepended value always has the current store value. Subscribe must accept a function which is called both immediately, and whenever the store value changes, it must return an unsubscribe function. The callback function must be passed the current store value as an argument whenever it is called.
- `set` - Automatically called whenever Svelte sees an assignment to a `$` prepended variable (like `$store = 'value'`). This should generally update the store value and call all subscribers.
### Custom stores
Now we know what Svelte needs to make use of the shorthand store syntax, we can get to work implementing a custom store by augmenting a svelte store and re-exporting it. Since Svelte doesn't care about additional methods being present on store objects, we are free to add whatever we like as long as `subscribe`, and optionally `set`, are present.
#### Linked stores
In this first example, we are creating a function that returns two linked stores that update when their partner changes, this example uses this linked store to convert temperatures from Celsius to Fahrenheit and vice-versa. The interface looks like this:
```js
store : { subscribe, set }
function(a_to_b_function, b_to_a_function): [store, store]
```
To implement this store, we need to create two writable stores, write custom `set` methods for each, and return an array of store objects containing this `set` method.
We define a function first as this implementation is a store _creator_ allowing us plenty of flexibility. The function needs to take two parameters, each being a callback function which is called when the stores are updated. The first function takes the first store value and returns a value that sets the value of the second store. The second argument does the opposite. One of these functions is called when the relevant `set` method is called.
```js
import { writable } from "svelte/store";
function synced(a_to_b, b_to_a) {
const a = writable();
const b = writable();
}
```
The `set` methods call their own `set` with the provided value and call the partner store's `set` when the provided value is passed through the callback function.
```js
// called when store_a.set is called or its binding reruns
function a_set($a) {
a.set($a);
b.set(a_to_b($a));
}
// called when store_b.set is called or its binding reruns
function b_set($b) {
a.set(b_to_a($b));
b.set($b);
}
```
All we need to do now is return an array of objects each containing the correct `subscribe` and `set` method:
```js
return [
{ subscribe: a.subscribe, set: a_set },
{ subscribe: b.subscribe, set: b_set },
];
```
Inside a component, we can use this synced store creator by deconstructing the returned array. This ensures Svelte can subscribe to each store individually, as stores definitions need to be at the top level for this to happen. This store can be imported and reused in any component.
```js
import { synced } from "./synced.js";
const [a, a_plus_five] = synced(
(a) => a + 5,
(b) => a - 5
);
$a = 0; // set an initial value
```
Since we have written custom `set` methods, we are also free to bind to each individual store. When one store updates, the other also updates after the provided function is applied to the value.
See it in action below. The following example uses the `synced` store to convert between Celsius and Fahrenheit in both directions.
```html
<script>
import { synced } from "./linkable";
export let initialCelsius = null;
export let initialFahrenheit = null;
const [C, F] = synced(
(C) => (C * 9) / 5 + 32,
(F) => ((F - 32) * 5) / 9
);
if (initialCelsius && initialFahrenheit) {
console.error(
"You can only set one initial temperature. Please set initialCelsius or initialFahrenheit but not both."
);
} else if (initialCelsius) {
$C = initialCelsius;
} else if (initialFahrenheit) {
$F = initialFahrenheit;
} else {
$C = 0;
}
</script>
<input bind:value="{$C}" type="number" /> ºC =
<input bind:value="{$F}" type="number" /> ºF
```
Play around with it in the [REPL](https://svelte.dev/repl/abbc56bdbd6e45c8ad5cd6f75108c6d8?version=3).
### a custom implementation of the builtin store
A simple store is about 20 lines of code, in many cases the built-in stores provide good primitives you can build on but sometimes it makes sense to write your own.
The most basic implementation would look something like this ([REPL](https://svelte.dev/repl/1c055b975b6d42f5b8623bad5d92e8fc?version=3.14.0)) (this is simpler than the built-in stores):
```js
function writable(init) {
let _val = init;
const subs = [];
const subscribe = (cb) => {
subs.push(cb);
cb(_val);
return () => {
const index = subs.findIndex((fn) => fn === cb);
subs.splice(index, 1);
};
};
const set = (v) => {
_val = v;
subs.forEach((fn) => fn(_val));
};
const update = (fn) => set(fn(_val));
return { subscribe, set, update };
}
```
From this point you could add whatever functionality you wanted.
Edit: Probably worth mentioning that this is a full writable implementation, only the subscribe method and its return value (an unsubscribe function) are required to be a valid store.

View File

@@ -0,0 +1,9 @@
---
title: Svelte Language Fundamentals
layout: recipeCategory
icon: "double-checkmark"
---
<!-- routify:options index=1 -->
Some description of this category should go here.

View File

@@ -0,0 +1,130 @@
---
title: Looping
layout: recipe
---
`{#each}` block allows you to loop only **array** or **array-like object** (i.e. it has a `.length` property).
Here are some examples if you want to loop through data structures besides array.
### Looping a map
You can use spread operator `[...value]` for [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) to get an array of key value pairs.
```svelte
<script>
const map = new Map([['.svelte', 'Svelte'], ['.js', 'JavaScript']]);
</script>
{#each [...map] as [key, value]}
<div>
{key}: {value}
</div>
{/each}
```
Both [`Map.keys()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/keys) and [`Map.values()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/values) method return an iterable. To use `{#each}` with iterable, you can use spread operator `[...value]` on the iterable.
```svelte
<script>
const map = new Map([['.svelte', 'Svelte'], ['.js', 'JavaScript']]);
</script>
{#each [...map.keys()] as key}
<div>
{key}
</div>
{/each}
{#each [...map.values()] as value}
<div>
{value}
</div>
{/each}
```
### Looping a set
You can use spread operator `[...value]` for [Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) to get an array of items.
```svelte
<script>
const set = new Set(['.svelte', '.js']);
</script>
{#each [...set] as item}
<div>
{item}
</div>
{/each}
```
### Looping a string
[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String) is considered an **array-like object** as it has `.length` property.
```svelte
<script>
const string = 'Svelte';
</script>
{#each string as character}
<div>
{character}
</div>
{/each}
```
### Looping a generator function
[Generator function `function*`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*) returns a [generator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator) object, which conforms to both the [iterable protocol](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#The_iterable_protocol) and the [iterator protocol](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#The_iterator_protocol).
To use `{#each}` with generator function, you can use spread operator `[...value]` on the generator.
```svelte
<script>
function* generator() {
yield '.svelte';
yield '.js';
}
</script>
{#each [...generator()] as item}
<div>
{item}
</div>
{/each}
```
### Binding to a spread item
Once you spread a Map, Set, Generator, or any Iterable, you are creating a new array, and therefore binding (`bind:`) with the item may not work anymore.
```svelte
<script>
const map = new Map([['.svelte', 'Svelte'], ['.js', 'JavaScript']]);
</script>
{#each [...map] as [key, value]}
<!-- You can't change the value of the input, nor the value in the map -->
<input bind:value={value} />
{/each}
```
To workaround this, you can use `on:input` listener
```svelte
<script>
const map = new Map([['.svelte', 'Svelte'], ['.js', 'JavaScript']]);
</script>
{#each [...map] as [key, value]}
<input
{value}
on:input={(event) => {
map.set(key, event.currentTarget.value);
map = map;
}}
/>
{/each}
```

View File

@@ -0,0 +1,23 @@
---
title: Using the `immutable` Compiler Option
layout: recipe
---
`<svelte:options immutable>` is a performance optimization you can add to your Svelte components.
There is no difference in compiled output size, but re-rendering mutable variables will be faster.
The official docs are [here](https://svelte.dev/docs#svelte_options)
and official example is [here](https://svelte.dev/examples#immutable-data).
The example demonstrates a compelling case to use the immutable flag when possible,
because it significantly optimizes child components in a list. This is likely one of the most common performance gotchas.
The parent component can opt into mutability, and use mutable data structures like maps, without de-optimizing all of its children.
- Objects, arrays, and functions are mutable variables in JavaScript.
- Mutations in Svelte are compiled to an `$$invalidate` function in the compiled output.
- The default option, `immutable: false`, will always treat calls to `$$invalidate` with mutable variables to always be true.
In other words, it will always rerender, to ensure correctness in case the variable was modified.
- Setting `immutable` to `true` opts out of this and relies on simple equality: `a !== b`.
You can see the difference between `safe_not_equal` and `not_equal` in [the source code here](https://github.com/sveltejs/svelte/blob/93313982991320c7bf2d65a037565b275addbea8/src/runtime/internal/utils.ts#L39).
This is the only place in Svelte affected by the `immutable` compiler option.

View File

@@ -0,0 +1,87 @@
---
title: Passing values from JS to CSS
layout: recipe
---
You can easily read in CSS media query values into JS with [`window.matchMedia`](https://developer.mozilla.org/en/docs/Web/API/Window/matchMedia). However, sometimes you want to pass information from JS to CSS variables, or have CSS Variables read into JS.
To set CSS Variables on an element, you can use the `style` attribute.
```svelte
<!-- App.svelte -->
<script>
import Child from './Child.svelte';
let backgroundColor = 'blue';
export let width = 30;
export let height = 30;
</script>
<label>Color <input type="color" bind:value={backgroundColor} /></label>
<label>Width <input type="range" bind:value={width} /></label>
<label>Height<input type="range" bind:value={height} /></label>
<div style="
--backgroundColor: {backgroundColor};
--width: {width}px;
--height: {height}px;
">
<Child />
</div>
<!-- Child.svelte -->
<style>
div {
background-color: var(--backgroundColor);
height: var(--height);
width: var(--width);
}
</style>
<div />
```
Alternatively, you can have a custom action to do this. This is already available with [svelte-css-vars](https://github.com/kaisermann/svelte-css-vars).
```svelte
<!-- App.svelte -->
<script>
import Circle from './Circle.svelte';
</script>
<Circle size="80x80" bg="url(https://placekitten.com/80/80) center" />
<Circle
size={120}
bg="radial-gradient(circle, #051937, #004d7a, #008793, #00bf72, #a8eb12) " />
<Circle
size={180}
bg="linear-gradient(45deg, #EE617D 25%, #3D6F8E 25%, #3D6F8E 50%, #EE617D 50%,
#EE617D 75%, #3D6F8E 75%, #3D6F8E 100%) center / 100% 20px" />
<Circle size="600x200" bg="url(https://placekitten.com/250/200) center" />
<!-- Circle.svelte -->
<script>
import cssVars from 'svelte-css-vars';
export let bg = 'black';
export let size = '50x50';
$: [width, height = width] = size.toString().split(/[x|\/]/);
$: styleVars = {
width: `${width}px`,
height: `${height}px`,
bg,
};
</script>
<style>
div {
display: inline-block;
width: var(--width);
height: var(--height);
background: var(--bg);
border-radius: 50%;
}
</style>
<div use:cssVars={styleVars} />
```

View File

@@ -0,0 +1,234 @@
---
title: Reactivity
layout: recipe
---
### Reactive assignments
The reactivity system introduced in Svelte 3 has made it easier than ever to trigger updates to the DOM. Despite this, there are a few simple rules that you must always follow. This guide explains how Svelte's reactivity system works, what you can and cannot do, as well a few pitfalls to avoid.
#### Top-level variables
The simplest way to make your Svelte components reactive is by using an assignment operator. Any time Svelte sees an assignment to a _top-level variable_ an update is scheduled. A 'top-level variable' is any variable that is defined inside the script element but is not a child of _anything_, meaning, it is not inside a function or a block. Incidentally, these are also the only variables that you can reference in the DOM. Let's look at some examples.
The following works as expected and update the dom:
```svelte
<script>
let num = 0;
function updateNum() {
num = 25;
}
</script>
<button on:click={updateNum}>Update</button>
<p>{num}</p>
```
Svelte can see that there is an assignment to a top-level variable and knows to re-render after the `num` variable is modified.
#### `each` blocks
> From **Svelte 3.23.1** onwards the following issue no longer applies. You can now assign to array item primitives and the changes will be reflected in the original array. See this [REPL](https://svelte.dev/repl/bc170b644f554eb29374138167dea4f0?version=3.23.1) running on **Svelte 3.23.1**, where the issue has been fixed. And this [REPL](https://svelte.dev/repl/bc170b644f554eb29374138167dea4f0?version=3.23.0) running on **Svelte 3.23.0** where the issue still applies. What follows is for archival purposes for anyone on **Svelte 3.23.0** and below. For further context see Svelte issue [4744](https://github.com/sveltejs/svelte/issues/4744).
The only exception to the top-level variable rule is when you are inside an `each` block. Any assignments to variables inside an `each` block trigger an update. Only assignments to array items that are objects or arrays result in the array itself updating. If the array items are primitives, the change is not traced back to the original array. This is because Svelte only reassigns the actual array items and primitives are passed by value in javascript, not by reference.
The following example causes the array and, subsequently, the DOM to be updated:
```svelte
<script>
let list = [{ n: 1 }, { n: 2 }, { n: 3 }];
</script>
{#each list as item}
<button on:click={() => item.n *= 2 }>{ item.n }</button>
{/each}
```
This, however, will not:
```svelte
<script>
let list = [1, 2, 3];
</script>
{#each list as item}
<button on:click={() => item *= 2 }>{ item }</button>
{/each}
```
The easiest workaround is to just reference the array item by index from inside the each block:
```svelte
<script>
let list = [1, 2, 3];
</script>
{#each list as item, index}
<button on:click={() => list[index] *= 2 }>{ item }</button>
{/each}
```
#### Variables not values
Svelte only cares about which _variables_ are being reassigned, not the values to which those variables refer. If the variable you reassign is not defined at the top level, Svelte does not trigger an update, even if the value you _are_ updating updates the original variable's value as well.
This is the sort of problem you may run into when dealing with objects. Since objects are passed by reference and not value, you can refer to the same value in many different variables. Let's look at an example:
```svelte
<script>
let obj = {
num: 0
};
function updateNum() {
const o = obj;
o.num = 25;
}
</script>
<button on:click={updateNum}>Update</button>
<p>{obj.num}</p>
```
In this example, when we reassign `o.num` we are updating the value assigned to `obj` but since we are not updating the actual `obj` variable SVelte does not trigger an update. Svelte does not trace these kinds of values back to a variable defined at the top level and has no way of knowing if it has updated or not. Whenever you want to update the local component state, any reassignments must be performed on the _actual_ variable, not just the value itself.
#### Shadowed variables
Another situation that can sometimes cause unexpected results is when you reassign a function's parameter (as above), and that parameter has the same _name_ as a top-level variable.
```svelte
<script>
let obj = {
num: 0
};
function(obj) {
obj.num = 25;
}
</script>
<button on:click={() => updateNum(obj)}>Update</button>
<p>{num}</p>
```
This example behaves the same as the previous example, except it is perhaps even more confusing. In this case, the `obj` variable is being _shadowed_ while inside the function, so any assignments to `obj` inside this function are assignments to the function parameter rather than the top-level `obj` variable. It refers to the same value, and it has the same name, but it is a _different_ variable inside the function scope.
Reassigning function parameters in this way is the same as reassigning a variable that points back to the top-level variable's value and does not cause an update. To avoid these problems, and potential confusion, it is a good idea not to reuse variable names in different scopes (such as inside functions), and always make sure that you are reassigning a top-level variable.
### Reactive Declarations
In addition to the assignment-based reactivity system, Svelte also has special syntax to define code that should rerun when its dependencies change using labeled statements - `$:`.
```svelte
<script>
let n = 0;
$: n_squared = n * n;
</script>
<button on:click={() => n += 1}>{ n_squared }</button>
```
Whenever Svelte sees a reactive declaration, it makes sure to execute any reactive statements that depend on one another in the correct order and only when their direct dependencies have changed. A 'direct dependency' is a variable that is referenced inside the reactive declaration itself. References to variables inside functions that a reactive declaration _calls_ are not considered dependencies.
```svelte
<script>
let n = 0;
const squareIt = () => n * n;
$: n_squared = squareIt();
</script>
<button on:click={() => n += 1}>{ n_squared }</button>
```
In the above example, `n_squared` will _not_ be recalculated when `n` changes because Svelte is not looking inside the `squareIt` function to define the reactive declaration's dependencies.
#### Defining dependencies
Sometimes you want to rerun a reactive declaration when a value changes but the variable itself is not required (in the case of some side-effects). The solution to this involves listing the dependency inside the declaration in some way.
The simplest solution is to pass the variable as an argument to a function, even if that variable is not required.
```js
let my_value = 0;
function someFunc() {
console.log(my_value);
}
$: someFunc(my_value);
```
In this example, `someFunc` doesn't require the `my_value` argument, but this lets Svelte know that `my_value` is a dependency of this reactive declaration and should rerun whenever it changes. In general, it makes sense to use any parameters that are passed in, even though all of these variables are in scope, it can make the code easier to follow.
```js
let my_value = 0;
function someFunc(value) {
console.log(value);
}
$: someFunc(my_value);
```
Here we have refactored `someFunc` to take an argument, making the code easier to follow.
If you need to list multiple dependencies and passing them as arguments is not an option, then the following pattern can be used:
```js
let one;
let two;
let three;
const someFunc = () => sideEffect();
$: one, two, three, someFunc();
```
This simple expression informs Svelte that it should rerun whenever `one`, `two`, or `three` changes. This expression runs the line of code whenever these dependencies change.
Similarly, if you only want to rerun a function when a variable changes _and_ is truthy then you can use the following pattern:
```js
let one;
const someFunc = () => sideEffect();
$: one && someFunc();
```
If the value of `one` is not truthy, then this short circuit expression stops at `one` and `someFunc` is not executed.
#### Variable deconstruction
Assigning a new value in a reactive declaration essentially turns these declarations into computed variables. However, deconstructing an object or array inside a reactive declaration may seem verbose or even impossible at first glance.
since `$: const { value } = object` is not valid javascript, we need to take a different approach. You can deconstruct values using reactive declarations by forcing javascript to interpret the statement as an expression.
To achieve this, all we need to do is wrap the declaration with parentheses:
```js
let some_obj = { one: 1, two: 2, three: 3 };
$: ({ one, two, three } = some_obj);
```
Javascript interprets this as an expression, deconstructing the value as expected with no unnecessary code required.
#### Hiding values from reactive declarations
On ocassion, you may wish to run a reactive declaration when only _some_ of its dependencies change, in essence, you need to _hide_ specific dependencies from Svelte. This is the opposite case of declaring additional dependencies and can be achieved by not referencing the dependencies in the reactive declaration but _hiding_ those references in a function.
```js
let one;
let two;
const some_func = (n) => console.log(n * two);
$: some_func(one);
```
The reactive declaration in this example reruns _only_ when `one` changes, we have hidden the reference to `two` inside a function because Svelte does not look inside of referenced functions to track dependencies.

View File

@@ -0,0 +1,21 @@
---
title: Scoped global CSS
layout: recipe
---
Sometimes your template code doesn't match your CSS. If you generate html via `{@html}` or have some un-styled elements inside child components, you might want to style it. However, Svelte won't let you write CSS that doesn't exist in the templates. You might feel forced to use `:global()` to make the CSS work, but that would leak it out to the rest of your app. So, instead you could try this trick:
```svelte
<div>
<Paragraph />
<Paragraph />
<Paragraph />
</div>
<style>
div :global(p + p) {
margin-top: 1rem;
}
</style>
```
Now that `p` styling will be output by Svelte, AND it won't leak out to the rest of your app.

View File

@@ -0,0 +1,14 @@
---
title: Benchmarking Svelte Components
layout: recipe
---
_This is a stub, if you are interested in this topic please reach out [via GitHub](https://github.com/svelte-society/sveltesociety.dev/issues/60) and lets work on this together._
1. Time taking for component to render.
2. Response time after event is fired
3. How much resources is the component consuming.
You may also be interested in this related benchmarking work:
- https://svelte-scaling.acmion.com/
- https://github.com/halfnelson/svelte-it-will-scale

View File

@@ -0,0 +1,9 @@
---
title: Testing and Debugging Svelte
layout: recipeCategory
icon: "bug"
---
<!-- routify:options index=1 -->
Some description of this category should go here.

View File

@@ -0,0 +1,154 @@
---
title: Unit Testing Svelte Components
layout: recipe
---
_Some assumptions here, this was pulled from something I wrote and the context was different. Edits will be required._
When testing svelte components in a node environment and using them outside of a Svelte application, we will be mostly interacting programmatically with Sveltes client-side API as well as the helpers found in @testing-library/svelte.
### Creating a component
Creating a component is as simple as passing the component constructor, and any initial props, to the provided render function.
```js
import { render } from "@testing-library/svelte";
import Button from "../src/Button.svelte";
test("should render", () => {
const results = render(Button, { props: { label: "a button" } });
expect(() => results.getByLabelText("a button")).not.toThrow();
});
```
`results` is an object containing a series of methods that can be used to query the rendered component in a variety of ways. You can see a list of all query methods in the testing-library documentation [link].
### Changing component props
A components props can be changed or set by calling the .\$set method of the component itself. results also has a component key which contains the component instance giving us access to Sveltes full client-side API.
Svelte schedules all component updates for completion asynchronously in the next micro-task. Awaiting the action that triggers that micro-task will ensure that the component has been updated before proceeding through the code. This also applies to any Svelte component update, regardless of whether it is programmatically changed (via component.\$set) or via a user action (such as clicking a button). For this to work we need to make sure that we make the tests callback function async, so we can use await in the body.
```js
import { render } from "@testing-library/svelte";
import Button from "../src/Button.svelte";
test("should render", async () => {
const { getByLabelText, component } = render(Button, {
props: { label: "a button" },
});
await component.$set({ label: "another button" });
expect(() => results.getByLabelText("another button")).not.toThrow();
});
```
### Testing component events
Component events have a different API to props and it is important we take the time to test them correctly. We will use a combination of jest mock functions and the svelte event API to make sure our component event is calling the provided handler. We can provide an event handler to a component event via a component's .$on method. The .$on method returns and off method, if we need to remove the listener.
```js
import { render, fireEvent } from "@testing-library/svelte";
import Button from "../src/Button.svelte";
test("events should work", () => {
const { getByLabelText, component } = render(Button, {
props: { label: "a button" },
});
const mock = Jest.fn();
const button = getByLabelText("a button");
component.$on("submit", mock);
fireEvent.click(button);
expect(mock).toHaveBeenCalled();
});
```
### Testing slots
Slots are more difficult to test as there is no programmatic interface for working with them either inside or outside of a Svelte component. The simplest way is to create a test specific component that utilises a dynamic component to and passes in a default slot to that component which can be asserted against. In this example the component passed as a prop will be used as the containing or parent component of the slotted child content.
```svelte
<script>
export let Component;
</script>
<svelte:component this={Component}>
<h1 data-testid="slot">Test Data</h1>
</svelte:component>
```
Then the component you wish to test can be passed to the constructor as a prop in order to mount the component correctly:
```js
import { render } from "@testing-library/svelte";
import SlotTest from "./SlotTest.svelte";
import ComponentToBeTested from "./ComponentToBeTested.svelte";
test("it should render slotted content", () => {
const { getByTestId } = render(SlotTest, {
props: { Component: ComponentToBeTested },
});
expect(getByTestId("slot")).not.toThrow();
});
```
### Testing the context API
As with slots, there is no programmatic interface for the Context API (setContext, getContext). If you are testing a component, in isolation, that would normally have a parent setting a context to be read by a child, then the simplest solution is to use a test specific parent. This is similar to the approach we used when testing slots. A test component might look something like this.
```svelte
<script>
import { setContext } from "svelte";
export let Component;
export let context_key;
export let context_value;
setContext(key, value);
</script>
<svelte:component this={Component} />
```
The component we wish to test looks something like this:
```svelte
<script>
import { KEY } from './Parent.svelte';
const ctx = getContext(KEY);
</script>
<button>{ctx.title}</button>
```
We can test this like so:
```js
import { render } from "@testing-library/svelte";
import ContextTest from "./ContextTest.svelte";
import ComponentToBeTested from "./ComponentToBeTested.svelte";
// Context is keyed, we need to get the actual key to ensure the correct context is selected by the child.
import { KEY } from "./OriginalParentComponent.svelte";
test("it should render slotted content", () => {
const { getByRole } = render(ContextTest, {
props: {
Component: ComponentToBeTested,
context_key: KEY,
context_value: { title: "Hello" },
},
});
const button = getByRole("button");
expect(button.innerHTML).toBe("Hello");
});
```
_to be written_

View File

@@ -0,0 +1,3 @@
<svg width="15" height="14" viewBox="0 0 15 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.18624 5.85097e-07C3.21583 -0.00156191 0 3.12656 0 6.9875C0 10.0406 2.0117 12.6359 4.8133 13.5891C5.1906 13.6813 5.1328 13.4203 5.1328 13.2422V12.0312C2.95413 12.2797 2.86583 10.8766 2.71972 10.6422C2.42431 10.1516 1.72592 10.0266 1.93463 9.79219C2.43073 9.54375 2.93647 9.85469 3.52248 10.6969C3.94633 11.3078 4.77316 11.2047 5.1922 11.1031C5.28372 10.7359 5.47959 10.4078 5.74931 10.1531C3.49197 9.75937 2.55115 8.41875 2.55115 6.825C2.55115 6.05156 2.81284 5.34063 3.32661 4.76719C2.99908 3.82188 3.35711 3.0125 3.40528 2.89219C4.33807 2.81094 5.3078 3.54219 5.38326 3.6C5.91307 3.46094 6.51835 3.3875 7.19587 3.3875C7.87661 3.3875 8.48349 3.46406 9.01812 3.60469C9.19954 3.47031 10.0986 2.84219 10.9656 2.91875C11.0122 3.03906 11.3622 3.82969 11.0539 4.7625C11.5741 5.3375 11.839 6.05469 11.839 6.82969C11.839 8.42656 10.8917 9.76875 8.62798 10.1562C8.82188 10.3418 8.97584 10.5631 9.08086 10.8073C9.18589 11.0514 9.23988 11.3135 9.23968 11.5781V13.3359C9.25252 13.4766 9.23968 13.6156 9.4805 13.6156C12.3239 12.6828 14.3709 10.0688 14.3709 6.98906C14.3709 3.12656 11.1534 5.85097e-07 7.18624 5.85097e-07Z" fill="#4A4A55"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
height="14px" viewBox="100 130 380 300">
<style type="text/css">
.st3{fill:#E24329;}
.st4{fill:#FCA326;}
.st5{fill:#FC6D26;}
</style>
<g id="logo_art">
<g>
<path id="path50_2_" class="st3" d="M293.026,434.983L293.026,434.983l62.199-191.322H230.918L293.026,434.983L293.026,434.983z"
/>
<path id="path66_6_" class="st4" d="M143.798,243.662L143.798,243.662l-18.941,58.126c-1.714,5.278,0.137,11.104,4.661,14.394
l163.509,118.801L143.798,243.662L143.798,243.662z"/>
<path id="path74_2_" class="st3" d="M143.798,243.662h87.12l-37.494-115.224c-1.919-5.895-10.282-5.895-12.27,0L143.798,243.662
L143.798,243.662z"/>
<path id="path82_6_" class="st4" d="M442.346,243.662L442.346,243.662l18.873,58.126c1.714,5.278-0.137,11.104-4.661,14.394
L293.026,434.983L442.346,243.662L442.346,243.662z"/>
<path id="path86_2_" class="st3" d="M442.346,243.662h-87.12l37.425-115.224c1.919-5.895,10.282-5.895,12.27,0L442.346,243.662
L442.346,243.662z"/>
<polygon class="st5" points="293.026,434.983 355.225,243.662 442.346,243.662 "/>
<polygon class="st5" points="293.026,434.983 143.798,243.662 230.918,243.662 "/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -9,7 +9,11 @@ const config = {
preprocess(),
mdsvex({
extensions: extensions,
layout: { eventPage: './src/lib/layouts/EventPage.svelte' }
layout: {
eventPage: './src/lib/layouts/EventPage.svelte',
recipe: './src/lib/layouts/Recipe.svelte',
recipeCategory: './src/lib/layouts/RecipeCategory.svelte'
}
})
],
extensions: extensions,