it feels absolutly bizzare how html forms have become framework dedicated solution over the years, and it becomes almost impossible to switch/jump between framework if a feature is not applicable somewhere or there's simply a library in another framework would make the job at hand go faster, this is what this library targets. Offering a fresh way to look at forms and how they're used without depending on any framework and offer framework dependant solutions wherever neccessary.
i need testers please, i also need help with svelte and vue frameworks as well, any help is much appreciated before the code source is available to the public somewhere in the next 2 months/early 2026
pnpm install @qundus/qform @qundus/qstate
you can create a form directly or make a form setup through createFormSetup, the difference is that the setup allows you to control all forms in your app from a single place. First let's take a look at creating a form directly:
// login-form | any file type js, cjs, mjs or ts
import { createForm } from "@qundus/qform";
export default createForm(
{
name: null, // fallback input.type="text"
password: "text" // place the type
}
);
that's it, you can just go ahead and use that in your app, but let's say you're making an app where all/any form fields across are optional, this would be a somewhat a hassle, this is where form setup comes in:
import { createFormSetup } from "@qundus/qform";
// consider it as making your own mini form maker/creator
export const createForm = createFormSetup({
fieldsRequired: false,
onEffect(props) {
// listen to all forms changes across your app
},
// ...other options ofcourse
})
hooks are the core that makes this library framework agnostic, you can install and import them individually, here's an example of how to create a form with framework specific hooks:
// setup/form.ts
import { createFormSetup } from "@qundus/qform";
import { preactHook } from "@qundus/qstate/preact";
export const createForm = createFormSetup({
storeHooks: {
useStore: preactHook,
},
})
// src/modules/auth/login-form.ts
import { createForm } from ":setup/form";
export default createForm(
{
name: null, // fallback input.type="text"
password: "text" // place the type
},
{
// you can also add the hooks here directly
storeHooks: {
useStore: preactHook,
},
}
);
// src/pages/index.html|astro|tsx|jsx|...
import loginForm from ":src/modules/auth/login-form";
// ...somewhere down the code
const state = loginForm.fields.name.store.hooks.useStore();
// ...use the hook
hooks are a massive part of qState please check it out.
Render is the api used to pass html element attributes, essentially it's what brings the input/component element to live with interactivity and other meta attributes like id, name, onclick...etc
Vanilla
<div class="flex flex-col h-fit self-start">
<label for="name" class="flex flex-row gap-2">
<input id="name" />
</label>
<label for="password" class="flex flex-row gap-2">
<input id="password" />
</label>
<script type="module">
import loginForm from "./login-form.mjs";
const name = document.getElementById("name");
const password = document.getElementById("password");
if (name != null) {
const atom = loginForm.fields.name.render.ref(name);
}
if (email != null) {
const atom = loginForm.fields.password.render.ref(email);
}
</script>
</div>
Preact React Solid tsx jsx
import loginForm from "./login-form";
interface Props extends IntrinsicElement<"input"> {}
export default function (props: Props) {
const field = loginForm.fields.name;
return (
<label className="flex flex-col gap-2">
<p>Name</p>
<input
{...props}
ref={field.render.ref}
// {...field.render.react()} // or use your hooks
/>
</label>
);
}
Svelte
<script lang="ts">
import loginForm from "./login-form";
const field = loginForm.fields.password;
const state = field.store;
// const state = atom.$store.hooks.useSvelte(); // or use your hooks
const props = $props();
</script>
<label class="flex flex-col gap-2">
<p>Password</p>
<input
{...props}
{...field.render.svelte.input}
class={`border-2px border-solid ${$state.element.focused "!border-black" : "border-gray"}`}
/>
{$state.errors?.[0]}
</label>
this is how you can listen to events occured on the form in general and on the element/field in particular from anywhere, weather it happened through dom events or through manual update.
for now form has only one type of events STATUS and that is to denote the general state of a form at any given moment.
import loginForm from "./login-form.mjs";
import { FORM } from "@qundus/qform/const"
loginForm.store.listen((value) => {
// use a if statement
if (value.status === FORM.STATUS.SUBMIT) {
// do something on submit
}
// or use a switch
switch (value.status) {
case FORM.STATUS.ERROR:
// do something on form error
break;
}
});
each field has independant set of events devided into 4 groups:
import loginForm from "./login-form.mjs";
import { FIELD } from "@qundus/qform/const"
form.fields.name.store.listen((value) => {
if (value.event.CYCLE === FIELD.CYCLE.SKELETON) {
// do something while field is in loading cycle
}
if (value.event.DOM === FIELD.DOM.CLICK) {
// do something when the field is clicked on
}
if (value.event.MUTATE === FIELD.MUTATE.VALUE) {
// do something when the field value has been mutated
}
if (value.event.RENDER === FIELD.RENDER.READY) {
// do something when the field render elements have been mounted
}
})
// you can also do it in framework specific effects
// for example, in react's effect
export default function Input() {
const field = loginForm.fields.name;
const state = field.store.hooks.react();
useEffect(() => {
if (state.event.CYCLE === FIELD.CYCLE.SKELETON) {
// do something while field is in loading cycle
}
}, [state]);
return <input {...field.render.react()} />
}
this describes the fields current status, there are 5 cycles that control the behavior or general state of the field, namely:
INIT: used during the setup of the fieldIDLE: idle status of the field where it accepts any changesSUBMIT: signals field under submit and won't be accepting any changesLOAD: user controlled status to denote loadingSKELETON: user controlled status to denote data loadingthis describes events that occur through html dom events, i won't be listing all of them here but you can always find them under FIELD.DOM constant.
any data change is hapening under a group/umbrella, you can listen or get notified of these data changes through the FIELD.MUTATE constant.
if the render api is used, it might be handy to know when the field has been mounted, all render elements start off with INIT status and changes to READY only when the field has been mounted onto any html element.
field options are refered to as setup options, here's a list of those setup options:
type
html input type of the field.
value
initial value, sometimes affects the final resulting field type as typescript type.
placeholder
input field placeholder.
label
label to better describe the field, falls back to field key.
labelReplace
when labels are taken from key because they're null, this replaces certain chars like '_' or '-' with ' '
validate
function or array of functions to validate value.
validateOn
some developers like to validate on field.blur and others on field.change.
onMount
fired once the field is mounted, you can use it fetch remote data or do any calculations per field mount.
onChange
fired everytime data changes, used in case of complex data values
need to be extracted from field element and the basic html.element.value attribute isn't accurate enough. some processors run when events like onfocus and onblur fire, this gives the chance to modify field state like required, disabled..etc, according to specific needs or logic.
be carefull here, if you set any values within this function it will rerun again so make sure you have solid if statments/conditions to avoid infinite loops/maximum stacks.
onRender
fired when html element is requesting render attributes, you can use this to attach any html attributes during runtime.
I chose to remove store mutations from here and keep the data handling within the previous onChange method/s, if you have solid reason why you need it here please open an issue on github.
hidden
mark field as hidden, this affects field type directly if the field is of type html.input. defaults to false.
required
mark field required, defaults to true.
disabled
mark field disabled, defaults to false.
mandatory
used for when a field is mandatory, like a checkbox.
initCycle
usually the cycle starts with INIT then IDLE moving to whatever developer logic prefers to set through onMount function, this alters the MOUNT cycle allowing for any cycle to replace it.
defaults to CYCLE.IDLE
vmcm
Value Manual Change Mechanism (VMCM), happens when values are updated from api fetch data or just manual programmatic interferrence. this affects whether the updated values go through the proper channels of validation, proccessing and affects on form status or just updates values without affecting anything else. defaults to "normal".
preprocessValue
all values go through preprocessing phase, use this to prevent that if you wish to have full controll over the input field and it's value, defaults to true
incompleteStatus
checks for missing/required value and mark condition.error as
incomplete. defaults to true
valueNullable
allowing the field value to be null
onChangeException
abort state changes when an exception is thrown from onChange method or not, defaults to false
props
user defined data/properties passed around
multiple
signals weather this field is requesting plural of whatever the value is or singular, defaults to false.
these are options specifically tailored to certain types since they require way more understanding and cannot just be categorized like any other input field/type. you can find them in the next chapter Extras
these options affect all the fields under assigned form.
vmcm
same as field.setup.vmcm
labelReplace
when labels are taken from key because they're null, this replaces certain chars like '_' or '-' with ' '
preprocessValues
global options to optin or out of values preprocessing based on field type. this option precedes individual ones.
preventErroredValues
usually all values are immediatly updated in the state, by setting this to true, only valid values will be commited. defaults to false
validateOn
wheather the preferred method of checking values is ran oninput or onchange, oninput checks for validation for every change happens, onchange checks once there's a state change like blur or focus. field specific validateOn takes precedence here.
defaults to input.
props
store and pass any data around
propsMergeStrategy
how's the props passed/merged with field's specific props. defaults to 'none'.
storeHooks
add hooks to be used by the store, relies on qState.
onMount
fires when all fields in the form have mounted
onEffect
fires when any field data changes, be carefull here, if you set any values within this function it will rerun again so make sure you have solid if statments/conditions to avoid infinite loops/maximum stacks.
fieldsRequired
default behavior of form is to consider all fields required, use this to change that default, indvidual fields 'required' options supercedes this. defaults to true
fieldsDisabled
default behavior of form is to consider all enabled, use this to change that default, 'disabled' options supercedes this. defaults to true.
fieldsInitCycle
default behavior of form is to start with CYCLES.IDLE, use this to change that default, indvidual fields 'initCycle' options supercedes this. defaults to CYCLE.INIT
fieldsOnChange
listen to all changes occured on any field and alter it's data if necessary, this gets called before the individual field's method if any.
fieldsOnRender
global per element render listener, gets called before or after field's specific onElement based on onFieldElementOrder option.
incompleteListCount
the last step of checking for form validity is to check for required fields' values and add them to the incompleteList, this option allows for choosing how big or small this list goes. defaults to false.
onUpdateKeyNotFound
how should the form react to updating values, defaults to "silent".
flatObjectKeysChar
if the desired data extracted from the form is in the shape of a nested object, then this splitter is used to determine which keys are nested, for now only the dot is supported. defaults to '.'.
flatLabelJoinChar
when no label is provided, the key is used as fallback but sometimes the key is meant to be unflattened later on when fetching the form data, so this offers a way to replace the key flatObjectKeysChar with any character. defaults to ' ' or empty space
some field/data types require more than the regular processing and that's due to their nature and expected user experience. Here we're going to explain eachone separately.
extras is the name given to the extras data/behavior generated based on such field types, extras offer all the necessary properties i think is standard when using those field types.
the entire behavior is set once the field's data type is set, and the those field types involved here are:
select.radioall the extras can be set through the previous chapter's field setup as regular field settings/options under the respective type's name, so for example, if you want to set checkbox's yes option you have to set field.checkbox.yes.
Finally, these extras will be found in any store object property, like field.store.get().extras or during field setup/settings onChange.$next.extras and validate.extras. This is could be very helpful while building components and what not.
CODE | where you can use extras
import { createForm } from "@qundus/qform";
export default createForm(
{
agreed: {
type: 'checkbox',
validate: ({value, extras}) => {
// validate value based on extras
},
onChange: ({ $next }) => {
const extras = $next.extras;
// do something with value based on extras
}
}
}
);
// later when you want to use form's data
import form from "../path/to/form";
const extras = field.store.get().extras;
// and ofcourse, element is set
const field = form.fields.agreed;
<input {...field.dom()} />
allows the user to change the standard on and off values retrieved by html standard input field when type is set to checkbox to whatever the user wants.
for example, if you wish to use boolean data type or just want the property value to be something when set to checked you can use the checkbox extras setup to do so.
API
yes
used to set the value of the field when checked, can be set to anything including objects, defaults to 'true'
no
used to set the value of the field when unchecked, can be set to anything including objects, defaults to 'false'
checked | readonly
use this to know the current state of the filed weather checked or not.
EXAMPLE
import { createForm } from "@qundus/qform";
export default createForm(
{
agreed: {
type: 'checkbox',
mandatory: false // here you can set weather this field is mandatory or not
preprocessValue: true // use this to cancel value preprocessing
checkbox: {
yes: true, // checked
no: "don't agree" // unchecked
}
}
}
);
// later when you want to use form's data
import form from "../path/to/form";
// value
const data = form.values.get();
console.log(data.agreed); // checked = true | not checked = "don't agree"
default behavior here is boolean data type, if preprocessValue is set to false then whatever data handled by the raw html input field/element will be set.
currently there's no special settings for this field type, it only offers extra data to deal with file input types.
API
count | readonly
offers file count when file/s uploaded, internally it loads the files buffers through a function and sets upload, failed and successful counts.
fallback | readonly, nullable
offers a fallback placeholders for the field/element, you can set value through field.setup option to a string and that would be processed here since value can only be a file/s.
files | readonly, nullable
offers all info regarding the uploaded files such as: buffers, progress, stage...etc. this would be very handy in building components.
It's worth noting that files array is updated asynchronously so you might notice multiple state updates when using this field type.
EXAMPLE
you can look into how the file component is build in the next chapter components.
import { createForm } from "@qundus/qform";
export default createForm(
{
picture: {
type: 'file',
value: '/placeholders/avatar.svg' // you can set an initial placeholder here, any string values will be trasnformed to extras.fallback
multiple: true // optinoal, affects final value
}
}
);
select fields have always been a little bit annoying to deal with when it comes to data handling, options array, what type of data to store, what key should we store as the value and so on. Here i've tried to simplify things as much as possible while keeping it flexible and scalable, essentially, you can set the options array to whatever you like and the final options array will always be adjusted to an array of objects, for example, let's say options array is set to [11, true, 'yeah'] then during runtime it will get normalized to an array of object with label and value -> {label: "11", value: 11}.
There's so much happening here to simplify the experience and i think it's better explained with the example below.
API
options
you can set the options array here and/or update it during onMount or onChange through the update addon/api, also you can set it to a certain type like null as unknown as {code: string}[] and that type would follow along as you use it.
valueKey
the value key used to determine the key to take value from within object, defaults to 'value'
labelKey
the value key used to determine the key to take value from within object, defaults to 'label'
throwOnKeyNotFound
safety option to throw errors if value or label keys are not found in object, default to false.
dynamic
used when options are set dynamically, for example you don't want to set the options array from the start but rather are using a select element with options and you wish those options whatever they maybe during runtime to be recorded as options internally, in other words, the options array are filled dynamically upon user selection in runtime. defaults to false
removeOnReselect
what should happen if an option is selected and it's clicked/chosen again?. by default, the option will get deselected, you can prevent that here by setting this option to false which will disallow option deselection. defaults to true
selected | readonly
offers currently selected index.
prev | readonly
offers previously selected options indecies.
current | readonly
offers currently selected options indecies.
RENDER
this field type has it's own special render method of applying to dom.
import form from "./path/to/form";
const field = form.fields.<your-field-name> // of type 'select'
// for triggers you can use
<div {...field.render.trigger()}>select</div>
// for options you can use
<option {...field.render.option(<selected-option>)}>option</option>
EXAMPLE
import { createForm } from "@qundus/qform";
export default createForm(
{
cities: {
type: 'select',
multiple: true, // optinoal, affects final value
select: {
options: ['sudan', 'saudi', {label: 'ir', name: 'iraq', __valueKey: 'name'}],// notice the per object valueKey renaming
// valueKey: 'value', // global valueKey change
// labelKey: 'value', // global valueKey change
}
}
}
);
// somewhere in your code, let's assume we're using solidjs
import form from "./path/to/form";
export function SelectCities() {
const field = form.fields.cities;
const state = field.store.hooks.solid();
return (
//
<div>
{/* value, assuming not multiples */}
<p {...field.render.trigger()}>{state.value?.[state.value.__labelKey ?? state.extras.labelKey]}</p>
{state.extras.options?.map((option) => {
return (
<option
class={option.selected? "text-blue" : "text-gray"}
{...field.render.solid.option(option)}
>{option[option.__labelKey ?? state.extras.labelKey]}</option>
)
}}
</div>
)
}
this does not check for when the field is set to have multiple in which case it will use extras.current array to loop over options and display the chosen value, for a broader example please check components chapter.
documentation coming soon, sorry i'm too busy
documentation coming soon, sorry i'm too busy
in this chapter i'm going to demonstrate how components are built with qform, this is a huge topic and while i'd really like to just dump everything i have here and explain it, i'm afraid this doc will get massively bigger, so until i build a website for this i'm going to stick to very few examples
let's start with a file uploader and say we're using preact framework here.
import { useStore } from '@nanostores/preact';
import { useEffect, useMemo, useRef } from 'preact/hooks';
import type * as _QFORM from '@qundus/qform';
interface Props<F extends _QFORM.Field.Setup<'file'>> extends ElementAttributesNoChildren<'input'> {
field: _QFORM.Field.Factory<F, any>;
description?: string;
buttonLabel?: string;
}
export default function FileUpload<F extends _QFORM.Field.Setup<'file'>>(props: Props<F>) {
const { field, description, buttonLabel, ...other } = props;
const state = useStore(field.store);
const ref = useRef<HTMLInputElement>(null);
function onInputClick(e: Event) {
//
e.preventDefault();
const clickEvent = new MouseEvent('click', {
view: window,
bubbles: true,
cancelable: true,
});
ref.current?.dispatchEvent(clickEvent);
}
return (
<div class="flex flex-col w-full group-left gap5 debug">
{/* input */}
<input
{...other}
{...field.render.preact()}
hidden
ref={(_ref) => {
ref.current = _ref;
}}
/>
{/* preview */}
<div
class="flex flex-col items-center justify-center min-w-64px min-h-64px w-64px h-64px max-w-64px max-h-64px cursor-pointer rounded-avatar"
onClick={onInputClick}
>
{state.extras.files || state.extras.fallback ? (
<img
src={state.extras.files?.[0]?.url ?? state.extras.fallback?.[0]?.url}
alt="avatar-placeholder"
class="object-cover w-full h-full rounded-avatar"
/>
) : (
<i class="icon-design:avatar w-full h-full" />
)}
</div>
{/* label/discription/button */}
<div class="flex flex-row flex-[1] items-start justify-between w-full h-full">
<p class="flex flex-col items-center justify-center gap1 py1">
{/* label */}
<span class="text-sm font-medium capitalize">{state.element.label}</span>
{state?.element.required && <span class="text-sm color-text-required ">*</span>}
</p>
<span class="flex flex-row w-full h-full style-text-secondary mt1">
{description ?? 'upload your file please'}
</span>
<div class="flex flex-row items-center justify-start gap2">
<button
class="min-w-71px min-h-32px h-32px max-h-32px text-sm font-medium style-btn-outline capitalize mt3"
onClick={onInputClick}
>
{buttonLabel ?? 'upload'}
</button>
{state.value != null && (
<button
class="min-w-71px min-h-32px h-32px max-h-32px text-sm font-medium style-btn-danger capitalize mt3"
onClick={(event) => {
event.preventDefault();
// event.stopPropagation();
// event.stopImmediatePropagation();
field.reset.value({ clear: true });
}}
>
{buttonLabel ?? 'Remove'}
</button>
)}
</div>
</div>
{state?.element.focused && state?.errors && (
<p class="flex flex-col items-center justify-center gap2 color-icon-fill-ds">
<i class="icon-local:alert?size=sm&stroke=sm"></i>
<span class="text-sm capitalize">{state?.errors?.[0]}</span>
</p>
)}
</div>
);
}
this example uses qform's types/namespaces to set it's interface, the type where it calls the setup _QFORM.Field.Setup<'file'>> is where the type magic happens, if you set the field type to anything other than file then the whole field object will just follow, you can ofcourse follow a whole different approach here and say you just want a regular component that has all the attributes to function like: onclick, id, name...etc, and that would be awesome for a more loosely-coupled integration with qform and wider inclusion of libraries but then you lose all the benefits qform offers such as extras, so it's up to you how you build your components :).
here's another example that builds a component for select element
import type * as _QFORM from '@qundus/qform';
import { useStore } from '@nanostores/preact';
import { useEffect, useMemo, useState } from 'preact/hooks';
import { FIELD } from '@qundus/qform/const';
interface Props<F extends _QFORM.Field.Setup<'select'>>
extends ElementAttributesNoChildren<'select', 'value'> {
field: _QFORM.Field.Factory<F, _QFORM.Form.Options>;
noLabel?: boolean;
}
export function Select<F extends _QFORM.Field.Setup<'select'>>(props: Props<F>) {
const { field, noLabel = false, value, ...other } = props;
const state = useStore(field?.store);
const [show, setShow] = useState(false);
useEffect(() => {
// console.log('wow :: ', state.value);
if (state.element.entered) {
// console.log('entered ', state.element);
showList();
}
if (state.event.DOM === FIELD.DOM.CLICK) {
setShow((prev) => !prev);
}
if (state.event.DOM === FIELD.DOM.BLUR) {
hideList();
}
}, [state]);
function showList() {
setShow(true);
}
function hideList() {
setShow(false);
}
return (
//
<label
class={
(state?.element.hidden ? ' hidden ' : ' wrap-col ') +
(state?.event.CYCLE === FIELD.CYCLE.SUBMIT || state.element.disabled
? ' opacity-50 '
: ' ') +
' w-full group-left gap2 '
}
>
{!noLabel && (
<p class="wrap-row group-center gap1 py1">
{/* label */}
<span class="text-sm font-medium capitalize">{state?.element.label}</span>
{state?.element.required && <span class="text-sm color-text-required ">*</span>}
</p>
)}
<div
class={
'relative wrap-row w-full group-between style-input style-input-height ' +
(state.condition.error
? 'style-input-error'
: state.element.focused
? 'style-input-focus'
: ' ')
}
{...field.render.preact.trigger()}
>
{state.value == null ? (
<p>Select an option</p>
) : !state.element.multiple ? (
<p>{state.value?.[state.value.__labelKey ?? state.extras.labelKey]}</p>
) : (
<div className="wrap-row group-left gap2">
{state.extras.current.map((index) => {
const option = state.extras.options[index];
return (
//
<p
key={option.__key}
className={
'wrap-row group-center w-fit h-full bg-bg-accent-gray-bolder text-sm text-text-accent-gray rounded-md gap2 px2 py1'
}
>
<span>{option[option.__labelKey ?? state.extras.labelKey]}</span>
<i
className="icon-local:close text-icon-outline-secondary z-50 cursor-pointer"
onClick={(event) => {
event.stopPropagation();
field.remove.option(option);
}}
></i>
</p>
);
})}
</div>
)}
<i
class={
'icon-local:chevron-down color-icon-outline-secondary ' + (show ? 'rotate-180' : '')
}
></i>
</div>
<div class="relative inline-block w-full h-inherit gap2">
<div
style={{ minHeight: '50px', maxHeight: '150px' }}
class={
(show ? 'flex flex-col' : 'hidden') +
' absolute z-1000 bg-bg-select w-full p0 style-input mt2 overflow-x-hidden overflow-y-auto'
}
>
{state.extras.options && state.extras.options.length > 0
? state.extras.options?.map((option) => {
const selected = option.__selected;
return (
<div
key={option.__key}
class={
'wrap-row w-full style-input-height items-center gap2 capitalize py2 ltr:(pr2 pl3) rtl:(pr3 pl2) cursor-pointer hover:(bg-bg-select-item-hover) ' +
(selected ? ' bg-bg-select-item-hover ' : '')
}
{...field.render.preact.option(option)}
>
{/* checkbox for multiple */}
{state.element.multiple && (
<div
class={
(other.class ?? '') +
' wrap-row w-10px h-10px min-w-10px min-h-10px overflow-hidden group-center style-checkbox ' +
(selected ? 'style-checkbox-checked' : '')
}
>
{selected && (
<i class="min-w-16px min-h-16px icon-local:check-3 color-icon-outline-brand" />
)}
</div>
)}
<span className="text-sm leading-6 w-full">
{option[option.__labelKey ?? state.extras.labelKey]}
</span>
{/* check mark for non-multiple */}
{!state.element.multiple && selected && (
<i class="min-w-16px min-h-16px icon-local:check-3 text-icon-fill-discovery mx2"></i>
)}
</div>
);
})
: 'no options offered'}
</div>
</div>
</label>
);
}
for now, i'm only going to demonstrate file component since i don't have time for more examples or guidelines but i think this is more than enough to pass the idea on how components are built, contact me or open an issue if you think more examples are necessary, thanx :)
addons are form and field helpers, for now they're forced (sorry) but in the future you'll be able to pick and choose which ones you want to add to form/field object.
this addon helps control submission and notifies all fields to move to submit cycle, usefull to place a lock on all form fields while marking it as submitting.
import loginForm from "../login-form";
// let's assume you're using preact hook
export function Page() {
async function submit(e: any) {
e.preventDefault();
// use possible to check if submission is possible
const ispossible = form.submit.possible();
// manually control submission cycle
const endSumbission = form.submit.start();
// use task to start a submission task
const [res, err] = await form.submit.task(() => {
return 'qform is awesome!'
});
if (err) {
console.error(err);
return
}
// successfull
console.log("success :: ", res)
}
return <button onClick={submit}>submit</button>
}
this addon offers an independant store that tailors to locking and unlocking form submit or action button, useful for disabling form submit button
import loginForm from "../login-form";
// let's assume you're using preact hook
export function Page() {
const $button = login.button.$store.hooks.preact();
async function submit(e: any) {
e.preventDefault();
const [res, err] = await form.submit.task(() => {
return 'qform is awesome!'
});
}
return (
<button
className="flex px-2 py-2 bg-green rounded-md !disabled:bg-red disabled:(cursor-not-allowed)"
disabled={$button.disabled}
onClick={submit}
>
submit
</button>
)
}
not all addons are listed here, i'll try to finish them soon.
converters are a way to use external schema validation libraries, converters work on forming the given schema object and flattening it and then creating the form fields.
Zod
import { form, cZod } from "@qundus/qform";
import { cZod } from "@qundus/qform/converters";
import { z } from "zod";
// schema data
enum Questions {
first = "are you cool?",
second = "COD or Battlefield?",
}
enum Colors {
red = "Red",
blue = "Blue",
}
const jobs = ["student", "employee"] as const;
const locations = ["saudi", "kuwait"] as const;
const schema = z.object({
// primitives
name: z.string(),
single: z.boolean(),
stringOrBoolean: z.string().nullable().or(z.boolean()), // not supported, fallsback to "text" field.type
huh: z.unknown().nullable(), // supports unknownTypes through unknownAsTextFields option
// objects
address: z.object({
street: z.string(),
zip: z.number(), // deep keys will be flattned address.zip
}),
// files
picture: z.instanceof(File),
documents: z.array(z.instanceof(File)),
// enums & nativeEnums
job: z.enum(jobs),
jobAndLocations: z.enum(jobs).and(z.enum(locations)), // collective enums
question: z.nativeEnum(Questions),
color: z.nativeEnum(Colors).nullable(),
questionOrColor: z.array(z.nativeEnum(Questions).and(z.nativeEnum(Colors))), // intersections and unions are treated the same for now
// arrays are not supported for now, you have to set the type yourself
// only enums, nativeEnums and files are supported for now
options: z.array(z.string()),
});
// now let's make our form fields
const fields = cZod.schemaToFields(schema, {
// override
override: {
huh: { valueNullable: false },
// for logically undefined types like array<string>
options: {
type: "select",
processValue: (value) => {
// tell the form how to process the value before it's validated
return value
},
},
},
});
// now pass it and create the form object
export const zodForm = form(fields);
and then use it how you would use the login from before.
extrasselect