web_lib

Common web application libraries
git clone https://radroots.dev/git/web_lib.git
Log | Files | Refs | LICENSE

commit 67f445fc60cf19ece5e0569cde2dcddf27f51adf
parent 4c92e1601e509bcd80ea29801cf7e0c16024f9d2
Author: triesap <triesap@radroots.dev>
Date:   Fri, 21 Nov 2025 02:59:04 +0000

apps-lib: add extended input component with reactive value binding, optional persistent sync, layered styling, and pluggable validation and interaction callbacks

Diffstat:
Aapps-lib/src/lib/components/input-ext.svelte | 117+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mapps-lib/src/lib/index.ts | 1+
2 files changed, 118 insertions(+), 0 deletions(-)

diff --git a/apps-lib/src/lib/components/input-ext.svelte b/apps-lib/src/lib/components/input-ext.svelte @@ -0,0 +1,117 @@ +<script lang="ts"> + import { browser } from "$app/environment"; + import type { IInput } from "$lib/types/components"; + import { fmt_cl, parse_layer, value_constrain } from "$lib/utils/app/lib"; + import { idb_kv } from "$lib/utils/keyval/lib"; + import { handle_err } from "@radroots/utils"; + + import { onMount } from "svelte"; + + let { + basis, + el = $bindable(null), + value = $bindable(``), + }: { + basis: IInput<string>; + el?: HTMLInputElement | null; + value?: string; + } = $props(); + + const id = $derived(basis?.id ? basis.id : null); + const layer = $derived( + typeof basis?.layer === `boolean` ? 0 : parse_layer(basis?.layer), + ); + const classes_layer = $derived( + typeof basis?.layer === `boolean` || typeof basis?.layer === `undefined` + ? `` + : `bg-ly${layer} text-ly${layer}-gl_d placeholder:text-ly${layer}-gl_pl caret-ly${layer}-gl`, + ); + + const sync_from_idb = async (): Promise<void> => { + if (!browser || !id) return; + try { + const kv_val = await idb_kv.get(id); + if (kv_val !== null && kv_val !== undefined && kv_val !== value) { + value = kv_val; + } else if (kv_val === null || kv_val === undefined) { + value = ``; + await idb_kv.set(id, ``); + } + } catch (e) { + handle_err(e, `sync_from_idb`); + } + }; + + const sync_to_idb = async (): Promise<void> => { + if (!browser || !id) return; + try { + await idb_kv.set(id, value || ``); + } catch (e) { + handle_err(e, `input_idb_sync`); + } + }; + + onMount(async () => { + await sync_from_idb(); + if (basis?.callback_mount && el) { + try { + await basis.callback_mount({ el }); + } catch (e) { + handle_err(e, `callback_mount`); + } + } + }); + + $effect(() => { + if (id && basis?.sync && browser) { + (async () => { + await sync_to_idb(); + })(); + } + }); + + const handle_on_input = async (): Promise<void> => { + try { + let val_cur = value; + let pass = true; + if (basis?.field) { + val_cur = value_constrain(basis.field?.charset, val_cur); + if (val_cur !== value) { + value = val_cur; + } + pass = basis.field?.validate.test(val_cur); + } + if (basis?.callback) { + await basis.callback({ value: val_cur, pass }); + } + } catch (e) { + handle_err(e, `handle_on_input`); + } + }; +</script> + +<input + bind:this={el} + bind:value + disabled={!!basis.disabled} + oninput={handle_on_input} + onblur={async ({ currentTarget: el }) => { + if (basis.callback_blur) await basis.callback_blur({ el }); + }} + onfocus={async ({ currentTarget: el }) => { + if (id && basis.sync && browser) await sync_from_idb(); + if (basis.callback_focus) await basis.callback_focus({ el }); + }} + onkeydown={async (ev) => { + if (basis?.callback_keydown) + await basis.callback_keydown({ + key: ev.key, + key_s: ev.key === `Enter`, + el: ev.currentTarget, + }); + }} + {id} + type="text" + class={`${fmt_cl(basis?.classes)} el-input ${classes_layer} el-re`} + placeholder={basis?.placeholder || ``} +/> diff --git a/apps-lib/src/lib/index.ts b/apps-lib/src/lib/index.ts @@ -26,4 +26,5 @@ export { default as Glyphi } from "./components/glyphi.svelte"; export { default as ImageBlob } from "./components/image-blob.svelte"; export { default as ImagePath } from "./components/image-path.svelte"; export { default as ImageSrc } from "./components/image-src.svelte"; +export { default as InputExt } from "./components/input-ext.svelte"; export { default as Input } from "./components/input.svelte";