Advent of SvelteKit 2022

Select items to compare

Challenge

Source code

+page.svelte

<script lang="ts">
	import type { PageData } from './$types';
	import type { Product } from '$lib/types';
	import { page } from '$app/stores';
	import { browser } from '$app/environment';
	import ProductSelect from './ProductSelect.svelte';
	import Comparison from './Comparison.svelte';

	export let data: PageData;

	$: products = data.products;

	// populate items from query params, when available
	let selectedId1 = $page.url.searchParams.get('item1');
	let selectedId2 = $page.url.searchParams.get('item2');
	let item1: Product | undefined = data.products.find((p) => p.id.toString() === selectedId1),
		item2: Product | undefined = data.products.find((p) => p.id.toString() === selectedId2);
</script>

<h1>Select items to compare</h1>

<form class="flow" on:submit|preventDefault>
	<ProductSelect id="i1" {products} bind:value={item1} name="item1">
		<svelte:fragment slot="label">Item 1</svelte:fragment>
	</ProductSelect>
	<ProductSelect id="i2" {products} bind:value={item2} name="item2">
		<svelte:fragment slot="label">Item 2</svelte:fragment>
	</ProductSelect>
	{#if !browser}
		<!-- For progressive enhancement, the form does not submit on enter when focusing a select 
        So we need an actual submit input. Hide it when JS is enabled (which unfortunately causes a layout shift)-->
		<input type="submit" />
	{/if}
</form>

{#if item1 && item2}
	{#if item1.id === item2.id}
		<p>These are the same items</p>
	{:else}
		<Comparison {item1} {item2} />
	{/if}
{/if}

<style>
	input {
		display: block;
	}
</style>

+page.ts

import type { PageLoad } from './$types';
import type { ProductsResult } from '$lib/types';

export const load: PageLoad = async () => {
	// https://dummyjson.com/docs/products
	const result: ProductsResult = await fetch('https://dummyjson.com/products').then((res) =>
		res.json()
	);
	return { ...result };
};

Comparison.svelte

<script lang="ts">
	import type { Product } from '$lib/types';

	export let item1: Product;
	export let item2: Product;

	let cheap: Product, expensive: Product;
	$: {
		if (item1.price < item2.price) {
			cheap = item1;
			expensive = item2;
		} else {
			cheap = item2;
			expensive = item1;
		}
	}

	$: multiplier = Math.floor(expensive.price / cheap.price);
</script>

<p>
	You can get <strong>{multiplier}x</strong> <em>{cheap.title}</em> for about the same price as a
	single
	<em>{expensive.title}</em>
</p>

ProductSelect.svelte

<script lang="ts">
	import type { Product } from '$lib/types';

	export let id: string;
	export let products: Product[];
	export let value: Product | undefined = undefined;
	export let name: string;

	let selectedId: number = value ? value.id : -1;

	// sync Product with the selected id
	// we bind to the ID since we need to use it like an actual form
	$: value = products.find((p) => p.id === selectedId);
</script>

<label for={id}><slot name="label" /></label>
<!-- Needed autocomplete off, otherwise Firefox would mess with the selected option 
https://stackoverflow.com/questions/4831848/firefox-ignores-option-selected-selected -->
<select {id} bind:value={selectedId} {name} autocomplete="off">
	<option disabled value={-1} selected={selectedId === -1}>Select an item</option>

	{#each products as p}
		<!-- Need the actual selected attribute for SSR -->
		<option value={p.id} selected={p.id === selectedId}>{p.title} - ${p.price}</option>
	{/each}
</select>

<style>
	label {
		display: block;
	}

	select {
		--flow-space: 0;
		padding: 0.5rem;
		border: 2px solid var(--gray-7);
	}
</style>