6 Commits

Author SHA1 Message Date
Elijah Duffy
ae7d4912f9 1.0.2 2026-02-13 16:28:35 -08:00
Elijah Duffy
1c66bc0fcf improve error handling with 'builder'-type structure 2026-02-13 16:28:25 -08:00
Elijah Duffy
3b659c1e2d 1.0.1 2026-02-04 14:54:21 -08:00
Elijah Duffy
90ff836061 text input: change default asterisk behaviour
asterisk now shown by default if field is required.
2026-02-04 14:53:14 -08:00
Elijah Duffy
5e5f133763 statemachine: add onsubmit callback 2026-02-04 11:19:03 -08:00
Elijah Duffy
7317d69d9b 1.0.0 2026-01-26 18:02:07 -08:00
5 changed files with 96 additions and 37 deletions

View File

@@ -4,7 +4,7 @@
"type": "git", "type": "git",
"url": "https://gitea.auvem.com/svelte-toolkit/sui.git" "url": "https://gitea.auvem.com/svelte-toolkit/sui.git"
}, },
"version": "0.3.5", "version": "1.0.2",
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
"build": "vite build && pnpm run prepack", "build": "vite build && pnpm run prepack",

View File

@@ -1,18 +1,28 @@
<script lang="ts"> <script lang="ts">
import type { ClassValue } from 'svelte/elements'; import type { ClassValue } from 'svelte/elements';
import type { ErrorMessage } from './error'; import { ErrorMessage, type RawError } from './error';
interface Props { interface Props {
error: ErrorMessage | null; /** Error in the form of an ErrorMessage */
error?: ErrorMessage | null;
/** Raw error that can be converted to an ErrorMessage */
rawError?: RawError | null;
/** Additional CSS classes for the error box */
class?: ClassValue | null; class?: ClassValue | null;
} }
let { error, class: classValue }: Props = $props(); let { error, rawError, class: classValue }: Props = $props();
let errorMessage = $derived.by(() => {
if (error) return error;
if (rawError) return new ErrorMessage(rawError);
});
</script> </script>
{#if error} {#if errorMessage && errorMessage.hasError()}
<!-- eslint-disable svelte/no-at-html-tags -->
<div class={['bg-sui-accent text-sui-background my-4 rounded-xs px-6 py-4', classValue]}> <div class={['bg-sui-accent text-sui-background my-4 rounded-xs px-6 py-4', classValue]}>
{@html error.message} {#each errorMessage.lines as line}
<p>{line}</p>
{/each}
</div> </div>
{/if} {/if}

View File

@@ -40,7 +40,7 @@
import { tweened } from 'svelte/motion'; import { tweened } from 'svelte/motion';
import { fade, fly } from 'svelte/transition'; import { fade, fly } from 'svelte/transition';
import { validateForm } from '@svelte-toolkit/validate'; import { validateForm } from '@svelte-toolkit/validate';
import { ArrowLeft, Check, CheckFat } from 'phosphor-svelte'; import { ArrowLeft, CheckFat } from 'phosphor-svelte';
import type { IconDef } from './util'; import type { IconDef } from './util';
interface Props { interface Props {
@@ -49,6 +49,13 @@
failure: StateMachinePage; failure: StateMachinePage;
index?: number; index?: number;
action?: string; action?: string;
/**
* Called when the form is submitted at the end of the state machine.
* Receives the collated form data as a key-value object.
* If you wish to prevent the submission, return false.
* If you wish to indicate a failure, you can also throw an error.
*/
onsubmit?: (data: Record<string, string>) => Promise<boolean> | boolean;
} }
let { let {
@@ -57,7 +64,8 @@
success, success,
failure, failure,
index = $bindable(0), index = $bindable(0),
action action,
onsubmit
}: Props = $props(); }: Props = $props();
// add success and failure pages to the end of the pages array // add success and failure pages to the end of the pages array
@@ -250,6 +258,22 @@
// update button state // update button state
buttonLoading = true; buttonLoading = true;
if (onsubmit) {
try {
const res = await onsubmit(collatedFormData);
if (res === false) {
buttonLoading = false;
index = pages.length - 2;
return;
}
} catch (e) {
console.log('onsubmit handler failed, submission prevented', e);
buttonLoading = false;
index = pages.length - 1;
return;
}
}
const form_data = new FormData(); const form_data = new FormData();
for (const [key, value] of Object.entries(collatedFormData)) { for (const [key, value] of Object.entries(collatedFormData)) {
form_data.append(key, value); form_data.append(key, value);

View File

@@ -11,7 +11,7 @@
value?: string; value?: string;
invalidMessage?: string | null; invalidMessage?: string | null;
ref?: HTMLInputElement | null; ref?: HTMLInputElement | null;
asterisk?: boolean; asterisk?: boolean | null;
class?: ClassValue | null | undefined; class?: ClassValue | null | undefined;
} }
@@ -21,12 +21,16 @@
value = $bindable(''), value = $bindable(''),
invalidMessage = 'Field is required', invalidMessage = 'Field is required',
ref = $bindable<HTMLInputElement | null>(null), ref = $bindable<HTMLInputElement | null>(null),
asterisk = false, asterisk = null,
class: classValue, class: classValue,
forceInvalid = false,
...others ...others
}: Props = $props(); }: Props = $props();
let valid: boolean = $state(true); let valid: boolean = $state(true);
let displayAsterisk = $derived(
asterisk === true || (asterisk !== false && others.validate && others.validate.required)
);
export const focus = () => { export const focus = () => {
if (ref) ref.focus(); if (ref) ref.focus();
@@ -37,7 +41,7 @@
{#if label} {#if label}
<Label for={id}> <Label for={id}>
{label} {label}
{#if asterisk} {#if displayAsterisk}
<span class="text-red-500">*</span> <span class="text-red-500">*</span>
{/if} {/if}
</Label> </Label>
@@ -50,11 +54,12 @@
onvalidate={(e) => { onvalidate={(e) => {
valid = e.detail.valid; valid = e.detail.valid;
}} }}
{forceInvalid}
{...others} {...others}
/> />
{#if others.validate && invalidMessage !== null} {#if others.validate && invalidMessage !== null}
<div class={['opacity-0 transition-opacity', !valid && 'opacity-100']}> <div class={['opacity-0 transition-opacity', (!valid || forceInvalid) && 'opacity-100']}>
<Label for={id} error> <Label for={id} error>
{invalidMessage} {invalidMessage}
</Label> </Label>

View File

@@ -10,20 +10,41 @@ export interface GraphError {
export type RawError = Error | string | GraphError[]; export type RawError = Error | string | GraphError[];
export class ErrorMessage { export class ErrorMessage {
private _message: string; private _lines: string[] = [];
/** converts a RawError to a string and stores it for later access */ /**
constructor(raw: RawError) { * Converts a RawError to an array of lines and stores it for later access,
this._message = ErrorMessage.rawErrorToString(raw); * or initializes without any errors if the input is null or undefined.
* @param raw The raw error to convert and store, or null/undefined for no error.
* @throws If the raw error is of an unsupported type.
*/
constructor(raw: RawError | null | undefined) {
if (raw) {
this._lines = ErrorMessage.rawErrorToLines(raw);
}
} }
/** returns the stored message */ /** returns the stored lines */
get message(): string { get lines(): string[] {
return this._message; return this._lines;
} }
/** returns the error as a string */ /** returns the error lines as a string, separated by newlines */
toString(): string { toString(): string {
return this._message; return this._lines.join('\n');
}
/** returns the error lines as an HTML string, separated by <br /> */
toHTML(): string {
return this._lines.join('<br />');
}
/** returns true if there are any error lines */
hasError(): boolean {
return this._lines.length > 0;
}
/** adds a new line to the error message */
addLine(line: string): void {
this._lines.push(line);
} }
/** optionally returns a new ErrorMessage only if the RawError is not empty */ /** optionally returns a new ErrorMessage only if the RawError is not empty */
@@ -32,28 +53,27 @@ export class ErrorMessage {
return new ErrorMessage(raw); return new ErrorMessage(raw);
} }
/** converts a RawError to a string */ /** converts a RawError to an array of lines */
static rawErrorToString(raw: RawError | null | undefined): string { static rawErrorToLines(raw: RawError | null | undefined): string[] {
if (!raw) return 'No error'; if (!raw) return ['No error'];
let errorString: string; let errorLines: string[];
if (typeof raw === 'string') { if (typeof raw === 'string') {
errorString = raw; errorLines = [raw];
} else if (raw instanceof Error) { } else if (raw instanceof Error) {
errorString = raw.message; errorLines = [raw.message];
} else if (Array.isArray(raw)) { } else if (Array.isArray(raw)) {
errorString = raw errorLines = raw.map((e) => {
.flatMap((e) => { const messageString = e.message || 'Unknown error';
const messageString = e.message || 'Unknown error'; if (e.path && e.path.length > 0) {
if (e.path && e.path.length > 0) { return `"${messageString}" at ${e.path.join('.')}`;
return `"${messageString}" at ${e.path.join('.')}`; }
} return messageString;
}) });
.join('<br />');
} else { } else {
throw `Bad error value ${raw}`; throw `Bad error value ${raw}`;
} }
return errorString; return errorLines;
} }
} }