Use zod preprocess to coerce string to number and string to boolean before performing zod validations. Using error messages on UI.

This commit is contained in:
Bradley Shellnut 2022-12-04 17:29:56 -08:00
parent 4bda219c22
commit 0c295c66b4
5 changed files with 54 additions and 32 deletions

View file

@ -3,20 +3,28 @@
import { boredState } from '$lib/stores/boredState'; import { boredState } from '$lib/stores/boredState';
export let form: ActionData; export let form: ActionData;
const errors = form?.errors;
let submitting = $boredState?.loading; let submitting = $boredState?.loading;
let minAge = +form?.minAge || 1; let minAge = +form?.data?.minAge || 1;
let minPlayers = +form?.minPlayers || 1; let minPlayers = +form?.data?.minPlayers || 1;
let maxPlayers = +form?.maxPlayers || 1; let maxPlayers = +form?.data?.maxPlayers || 1;
let exactMinPlayers = form?.exactMinPlayers || false; let exactMinPlayers = Boolean(form?.data?.exactMinPlayers) || false;
let exactMaxPlayers = form?.exactMaxPlayers || false; let exactMaxPlayers = Boolean(form?.data?.exactMaxPlayers) || false;
</script> </script>
<fieldset class="advanced-search" aria-busy={submitting} disabled={submitting}> <fieldset class="advanced-search" aria-busy={submitting} disabled={submitting}>
<div> <div>
<label for="minAge"> <label for="minAge">
Min Age Min Age
<input id="minAge" name="minAge" bind:value={minAge} type="number" min="1" max="120" /> <input id="minAge" name="minAge" bind:value={minAge} type="number" min={1} max={120} />
</label> </label>
{#if form?.errors?.minAge}
<div id="minPlayers-error" class="error">
<p aria-label={`Error: ${form?.errors?.minAge}`} class="center">
{form?.errors?.minAge}
</p>
</div>
{/if}
</div> </div>
<div> <div>
<label for="minPlayers"> <label for="minPlayers">
@ -26,8 +34,8 @@
name="minPlayers" name="minPlayers"
bind:value={minPlayers} bind:value={minPlayers}
type="number" type="number"
min="1" min={1}
max="50" max={50}
/> />
</label> </label>
<label for="exactMinPlayers" style="display: flex; gap: 1rem; place-items: center;"> <label for="exactMinPlayers" style="display: flex; gap: 1rem; place-items: center;">
@ -37,12 +45,13 @@
type="checkbox" type="checkbox"
name="exactMinPlayers" name="exactMinPlayers"
bind:checked={exactMinPlayers} bind:checked={exactMinPlayers}
bind:value={exactMinPlayers}
/> />
</label> </label>
{#if form?.error?.id === 'minPlayers'} {#if form?.errors?.minPlayers}
<div id="minPlayers-error" class="error"> <div id="minPlayers-error" class="error">
<p aria-label={`Error: ${form.error.message}`} class="center"> <p aria-label={`Error: ${form?.errors?.minPlayers}`} class="center">
Error: {form.error.message} {form?.errors?.minPlayers}
</p> </p>
</div> </div>
{/if} {/if}
@ -55,8 +64,8 @@
name="maxPlayers" name="maxPlayers"
bind:value={maxPlayers} bind:value={maxPlayers}
type="number" type="number"
min="1" min={1}
max="50" max={50}
/> />
</label> </label>
<label for="exactMaxPlayers" style="display: flex; gap: 1rem; place-items: center;"> <label for="exactMaxPlayers" style="display: flex; gap: 1rem; place-items: center;">
@ -66,6 +75,7 @@
type="checkbox" type="checkbox"
name="exactMaxPlayers" name="exactMaxPlayers"
bind:checked={exactMaxPlayers} bind:checked={exactMaxPlayers}
bind:value={exactMaxPlayers}
/> />
</label> </label>
{#if form?.error?.id === 'maxPlayers'} {#if form?.error?.id === 'maxPlayers'}

View file

@ -25,6 +25,7 @@
// console.log('search page data', data); // console.log('search page data', data);
export let form: ActionData; export let form: ActionData;
// console.log('search page form', form); // console.log('search page form', form);
const errors = form?.errors;
export let showButton: boolean = false; export let showButton: boolean = false;
export let advancedSearch: boolean = false; export let advancedSearch: boolean = false;
@ -37,7 +38,7 @@
let totalItems = form?.totalCount || data?.totalCount || 0; let totalItems = form?.totalCount || data?.totalCount || 0;
let submitting = $boredState?.loading; let submitting = $boredState?.loading;
let name = form?.name || ''; let name = form?.name || '';
let disclosureOpen = false; let disclosureOpen = errors || false;
$: skip = (page - 1) * pageSize; $: skip = (page - 1) * pageSize;
$: showPagination = $gameStore?.length > 1; $: showPagination = $gameStore?.length > 1;

View file

@ -1,4 +1,4 @@
import { z } from 'zod'; import { z, ZodNumber, ZodOptional } from 'zod';
import zodToJsonSchema from 'zod-to-json-schema'; import zodToJsonSchema from 'zod-to-json-schema';
export const BoardGameSearch = z.object({ export const BoardGameSearch = z.object({
@ -16,14 +16,30 @@ export const saved_game_schema = z.object({
playtime: z.string() playtime: z.string()
}); });
// https://github.com/colinhacks/zod/discussions/330
// function IntegerString
// <schema extends (ZodNumber | ZodOptional<ZodNumber>)>
// (schema: schema)
// {
// return (
// z.preprocess((value) => (
// ( (typeof value === "string") ? parseInt(value, 10)
// : (typeof value === "number") ? value
// : undefined
// )), schema)
// )
// }
// minPlayers: IntegerString(z.number().min(1).max(50).optional());
export const search_schema = z.object({ export const search_schema = z.object({
name: z.string().trim().optional(), name: z.string().trim().optional(),
minAge: z.number().min(1).max(120).optional(), minAge: z.preprocess((a) => parseInt(z.string().parse(a), 10), z.number().min(1).max(120).optional()),
minPlayers: z.number().min(1).max(50).optional(), minPlayers: z.preprocess((a) => parseInt(z.string().parse(a), 10), z.number().min(1).max(50).optional()),
maxPlayers: z.number().min(1).max(50).optional(), maxPlayers: z.preprocess((a) => parseInt(z.string().parse(a), 10), z.number().min(1).max(50).optional()),
exactMinAge: z.boolean().optional(), exactMinAge: z.preprocess((a) => Boolean(a), z.boolean().optional()),
exactMinPlayers: z.boolean().optional(), exactMinPlayers: z.preprocess((a) => Boolean(a), z.boolean().optional()),
exactMaxPlayers: z.boolean().optional(), exactMaxPlayers: z.preprocess((a) => Boolean(a), z.boolean().optional())
}) })
.superRefine(({ minPlayers, maxPlayers, minAge, exactMinAge, exactMinPlayers, exactMaxPlayers }, ctx) => { .superRefine(({ minPlayers, maxPlayers, minAge, exactMinAge, exactMinPlayers, exactMaxPlayers }, ctx) => {
console.log({ minPlayers, maxPlayers }); console.log({ minPlayers, maxPlayers });

View file

@ -5,7 +5,9 @@
import Random from '$lib/components/random/index.svelte'; import Random from '$lib/components/random/index.svelte';
export let data: PageData; export let data: PageData;
console.log('data', data);
export let form: ActionData; export let form: ActionData;
console.log('form', form);
</script> </script>
<svelte:head> <svelte:head>

View file

@ -18,6 +18,7 @@ export const actions: Actions = {
console.log("In search action specific") console.log("In search action specific")
// Do things in here // Do things in here
const formData = Object.fromEntries(await request.formData()); const formData = Object.fromEntries(await request.formData());
console.log('formData', formData);
console.log('passed in limit:', formData?.limit) console.log('passed in limit:', formData?.limit)
console.log('passed in skip:', formData?.skip) console.log('passed in skip:', formData?.skip)
const limit = formData?.limit || 10; const limit = formData?.limit || 10;
@ -26,8 +27,8 @@ export const actions: Actions = {
const queryParams: SearchQuery = { const queryParams: SearchQuery = {
order_by: 'rank', order_by: 'rank',
ascending: false, ascending: false,
limit: parseInt(limit), limit: +limit,
skip: parseInt(skip), skip: +skip,
client_id: BOARD_GAME_ATLAS_CLIENT_ID, client_id: BOARD_GAME_ATLAS_CLIENT_ID,
fuzzy_match: true, fuzzy_match: true,
name: '' name: ''
@ -40,14 +41,8 @@ export const actions: Actions = {
if (random) { if (random) {
queryParams.random = random; queryParams.random = random;
} else { } else {
// const minAge = form.get('minAge');
// const minPlayers = form.get('minPlayers');
// const maxPlayers = form.get('maxPlayers');
// const exactMinAge = form.get('exactMinAge') || false;
// const exactMinPlayers = form.get('exactMinPlayers') || false;
// const exactMaxPlayers = form.get('exactMaxPlayers') || false;
try { try {
console.log('Parsed', search_schema.parse(formData))
const { const {
name, name,
minAge, minAge,
@ -58,7 +53,6 @@ export const actions: Actions = {
exactMaxPlayers exactMaxPlayers
} = search_schema.parse(formData); } = search_schema.parse(formData);
console.log('Parsed', search_schema.parse(formData))
if (minAge) { if (minAge) {
if (exactMinAge) { if (exactMinAge) {
@ -87,7 +81,6 @@ export const actions: Actions = {
queryParams.name = name; queryParams.name = name;
} }
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof ZodError) { if (error instanceof ZodError) {
const { fieldErrors: errors } = error.flatten(); const { fieldErrors: errors } = error.flatten();
return invalid(400, { data: formData, errors }); return invalid(400, { data: formData, errors });