UNPKG

sveltekit-superforms

Version:

Making SvelteKit forms a pleasure to use!

492 lines (491 loc) 16.9 kB
/* eslint-disable @typescript-eslint/no-explicit-any */ import { derived, get, writable } from 'svelte/store'; import { SuperFormError } from '../errors.js'; import { pathExists, traversePath } from '../traversal.js'; import { splitPath } from '../stringPath.js'; import { browser } from '$app/environment'; const defaultOptions = { trueStringValue: 'true', dateFormat: 'iso', step: 60 }; ///// Proxy functions /////////////////////////////////////////////// export function booleanProxy(form, path, options) { return _stringProxy(form, path, 'boolean', { ...defaultOptions, ...options }); } export function intProxy(form, path, options) { return _stringProxy(form, path, 'int', { ...defaultOptions, ...options }); } export function numberProxy(form, path, options) { return _stringProxy(form, path, 'number', { ...defaultOptions, ...options }); } export function dateProxy(form, path, options) { return _stringProxy(form, path, 'date', { ...defaultOptions, dateFormat: options?.format ?? 'iso', empty: options?.empty, step: options?.step ?? 60 }); } export function stringProxy(form, path, options) { return _stringProxy(form, path, 'string', { ...defaultOptions, ...options }); } export function fileFieldProxy(form, path, options) { const fileField = fileProxy(form, path, options); const formField = formFieldProxy(form, path, options); return { ...formField, value: fileField }; } export function fileProxy(form, path, options) { const formFile = fieldProxy(form, path, options); const fileProxy = writable(browser ? new DataTransfer().files : {}); let initialized = false; let initialValue; formFile.subscribe((file) => { if (!browser) return; if (!initialized) { initialValue = options?.empty ? (options.empty === 'undefined' ? undefined : null) : file; initialized = true; } const dt = new DataTransfer(); if (file instanceof File) dt.items.add(file); fileProxy.set(dt.files); }); const fileStore = { subscribe(run) { return fileProxy.subscribe(run); }, set(file) { if (!browser) return; if (!file) { const dt = new DataTransfer(); fileProxy.set(dt.files); formFile.set(file); } else if (file instanceof File) { const dt = new DataTransfer(); dt.items.add(file); fileProxy.set(dt.files); formFile.set(file); } else if (file instanceof FileList) { fileProxy.set(file); if (file.length > 0) formFile.set(file.item(0)); else formFile.set(initialValue); } }, update() { throw new SuperFormError('You cannot update a fileProxy, only set it.'); } }; return fileStore; } export function filesFieldProxy(form, path, options) { const filesStore = filesProxy(form, path, options); const arrayField = arrayProxy(form, path, options); return { ...arrayField, values: filesStore }; } export function filesProxy(form, path, options) { const formFiles = fieldProxy(form, path, options); const filesProxy = writable(browser ? new DataTransfer().files : {}); formFiles.subscribe((files) => { if (!browser) return; const dt = new DataTransfer(); if (Array.isArray(files)) { if (files.length && files.every((f) => !f)) { formFiles.set([]); return; } files.filter((f) => f instanceof File).forEach((file) => dt.items.add(file)); } filesProxy.set(dt.files); }); const filesStore = { subscribe(run) { return filesProxy.subscribe(run); }, set(files) { if (!browser) return; if (!(files instanceof FileList)) { const dt = new DataTransfer(); if (Array.isArray(files)) files.forEach((file) => { if (file instanceof File) dt.items.add(file); }); filesProxy.set(dt.files); formFiles.set(files); } else { const output = []; for (let i = 0; i < files.length; i++) { const file = files.item(i); if (file) output.push(file); } filesProxy.set(files); formFiles.set(output); } }, update(updater) { filesStore.set(updater(get(formFiles))); } }; return filesStore; } ///// Implementation //////////////////////////////////////////////// /** * Creates a string store that will pass its value to a field in the form. * @param form The form * @param field Form field * @param type 'number' | 'int' | 'boolean' */ function _stringProxy(form, path, type, options) { function toValue(value) { if (!value && options.empty !== undefined) { return options.empty === 'null' ? null : options.empty === 'zero' ? 0 : undefined; } if (typeof value === 'number') { value = value.toString(); } if (typeof value !== 'string') { // Can be undefined due to Proxy in Svelte 5 value = ''; } const stringValue = value; if (type == 'string') return stringValue; else if (type == 'boolean') return !!stringValue; else if (type == 'date') { if (stringValue.indexOf('-') === -1) { const utc = options.dateFormat.indexOf('utc') >= 0; const date = utc ? UTCDate(new Date()) : localDate(new Date()); return new Date(date + 'T' + stringValue + (utc ? 'Z' : '')); } else return new Date(stringValue); } const numberToConvert = options.delimiter ? stringValue.replace(options.delimiter, '.') : stringValue; let num; if (numberToConvert === '' && options.empty == 'zero') num = 0; else if (type == 'number') num = parseFloat(numberToConvert); else num = parseInt(numberToConvert, 10); return num; } const isSuper = isSuperForm(form, options); const realProxy = isSuper ? superFieldProxy(form, path, { taint: options.taint }) : fieldProxy(form, path); let updatedValue = null; let initialized = false; const proxy = derived(realProxy, (value) => { if (!initialized) { initialized = true; if (options.initiallyEmptyIfZero && !value) return ''; } // Prevent proxy updating itself if (updatedValue !== null) { const current = updatedValue; updatedValue = null; return current; } if (value === undefined || value === null) return ''; if (type == 'string') { return value; } else if (type == 'int' || type == 'number') { if (value === '') { // Special case for empty string values in number proxies // Set the value to 0, to conform to the type. realProxy.set(0, isSuper ? { taint: false } : undefined); } if (typeof value === 'number' && isNaN(value)) return ''; return String(value); } else if (type == 'date') { const date = typeof value === 'string' || typeof value === 'number' ? new Date(value) : value; if (isNaN(date)) return ''; switch (options.dateFormat) { case 'iso': return date.toISOString(); case 'date': return date.toISOString().slice(0, 10); case 'datetime': return date.toISOString().slice(0, options.step % 60 ? 19 : 16); case 'time': return date.toISOString().slice(11, options.step % 60 ? 19 : 16); case 'date-utc': return UTCDate(date); case 'datetime-utc': return UTCDate(date) + 'T' + UTCTime(date, options.step); case 'time-utc': return UTCTime(date, options.step); case 'date-local': return localDate(date); case 'datetime-local': return localDate(date) + 'T' + localTime(date, options.step); case 'time-local': return localTime(date, options.step); } } else { // boolean return value ? options.trueStringValue : ''; } }); return { subscribe: proxy.subscribe, set(val) { updatedValue = val; const newValue = toValue(updatedValue); realProxy.set(newValue); }, update(updater) { realProxy.update((f) => { updatedValue = updater(String(f)); const newValue = toValue(updatedValue); return newValue; }); } }; } export function arrayProxy(superForm, path, options) { const formErrors = fieldProxy(superForm.errors, `${path}`); const onlyFieldErrors = derived(formErrors, ($errors) => { const output = []; for (const key in $errors) { if (key == '_errors') continue; output[key] = $errors[key]; } return output; }); function updateArrayErrors(errors, value) { for (const key in errors) { if (key == '_errors') continue; errors[key] = undefined; } if (value !== undefined) { for (const key in value) { errors[key] = value[key]; } } return errors; } const fieldErrors = { subscribe: onlyFieldErrors.subscribe, update(upd) { formErrors.update(($errors) => // @ts-expect-error Type is correct updateArrayErrors($errors, upd($errors))); }, set(value) { // @ts-expect-error Type is correct formErrors.update(($errors) => updateArrayErrors($errors, value)); } }; const values = superFieldProxy(superForm, path, options); // If array is shortened, delete all keys above length // in errors, so they won't be kept if the array is lengthened again. let lastLength = Array.isArray(get(values)) ? get(values).length : 0; values.subscribe(($values) => { const currentLength = Array.isArray($values) ? $values.length : 0; if (currentLength < lastLength) { superForm.errors.update(($errors) => { const node = pathExists($errors, splitPath(path)); if (!node) return $errors; for (const key in node.value) { if (Number(key) < currentLength) continue; delete node.value[key]; } return $errors; }, { force: true }); } lastLength = currentLength; }); return { path, values: values, errors: fieldProxy(superForm.errors, `${path}._errors`), valueErrors: fieldErrors }; } export function formFieldProxy(superForm, path, options) { const path2 = splitPath(path); // Filter out array indices, the constraints structure doesn't contain these. const constraintsPath = path2.filter((p) => /\D/.test(String(p))).join('.'); const taintedProxy = derived(superForm.tainted, ($tainted) => { if (!$tainted) return $tainted; const taintedPath = traversePath($tainted, path2); return taintedPath ? taintedPath.value : undefined; }); const tainted = { subscribe: taintedProxy.subscribe, update(upd) { superForm.tainted.update(($tainted) => { if (!$tainted) $tainted = {}; const output = traversePath($tainted, path2, (path) => { if (!path.value) path.parent[path.key] = {}; return path.parent[path.key]; }); if (output) output.parent[output.key] = upd(output.value); return $tainted; }); }, set(value) { superForm.tainted.update(($tainted) => { if (!$tainted) $tainted = {}; const output = traversePath($tainted, path2, (path) => { if (!path.value) path.parent[path.key] = {}; return path.parent[path.key]; }); if (output) output.parent[output.key] = value; return $tainted; }); } }; return { path, value: superFieldProxy(superForm, path, options), errors: fieldProxy(superForm.errors, path), constraints: fieldProxy(superForm.constraints, constraintsPath), tainted }; } function updateProxyField(obj, path, updater) { const output = traversePath(obj, path, ({ parent, key, value }) => { if (value === undefined) parent[key] = /\D/.test(key) ? {} : []; return parent[key]; }); if (output) { const newValue = updater(output.value); output.parent[output.key] = newValue; } return obj; } function superFieldProxy(superForm, path, baseOptions) { const form = superForm.form; const path2 = splitPath(path); const proxy = derived(form, ($form) => { const data = traversePath($form, path2); return data?.value; }); return { subscribe(...params) { const unsub = proxy.subscribe(...params); return () => unsub(); }, update(upd, options) { form.update((data) => updateProxyField(data, path2, upd), options ?? baseOptions); }, set(value, options) { form.update((data) => updateProxyField(data, path2, () => value), options ?? baseOptions); } }; } function isSuperForm(form, options) { const isSuperForm = 'form' in form; if (!isSuperForm && options?.taint !== undefined) { throw new SuperFormError('If options.taint is set, the whole superForm object must be used as a proxy.'); } return isSuperForm; } export function fieldProxy(form, path, options) { const path2 = splitPath(path); if (isSuperForm(form, options)) { return superFieldProxy(form, path, options); } const proxy = derived(form, ($form) => { const data = traversePath($form, path2); return data?.value; }); return { subscribe(...params) { const unsub = proxy.subscribe(...params); return () => unsub(); }, update(upd) { form.update((data) => updateProxyField(data, path2, upd)); }, set(value) { form.update((data) => updateProxyField(data, path2, () => value)); } }; } function localDate(date) { return (date.getFullYear() + '-' + String(date.getMonth() + 1).padStart(2, '0') + '-' + String(date.getDate()).padStart(2, '0')); } function localTime(date, step) { return (String(date.getHours()).padStart(2, '0') + ':' + String(date.getMinutes()).padStart(2, '0') + (step % 60 ? ':' + String(date.getSeconds()).padStart(2, '0') : '')); } function UTCDate(date) { return (date.getUTCFullYear() + '-' + String(date.getUTCMonth() + 1).padStart(2, '0') + '-' + String(date.getUTCDate()).padStart(2, '0')); } function UTCTime(date, step) { return (String(date.getUTCHours()).padStart(2, '0') + ':' + String(date.getUTCMinutes()).padStart(2, '0') + (step % 60 ? ':' + String(date.getUTCSeconds()).padStart(2, '0') : '')); } /* function dateToUTC(date: Date) { return new Date( date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds() ); } */