feat: implement form validation hook
parent
3abc6a2779
commit
e11c8449b9
@ -0,0 +1,154 @@
|
|||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
createEffect,
|
||||||
|
createSignal,
|
||||||
|
FlowComponent,
|
||||||
|
onCleanup,
|
||||||
|
onMount,
|
||||||
|
useContext,
|
||||||
|
} from "solid-js";
|
||||||
|
import { createStore } from "solid-js/store";
|
||||||
|
|
||||||
|
const makeStore = () =>
|
||||||
|
createStore({
|
||||||
|
valid: true as boolean,
|
||||||
|
values: {} as Record<string, any>,
|
||||||
|
errors: {} as Record<string, ValidityState>,
|
||||||
|
});
|
||||||
|
type Store = ReturnType<typeof makeStore>;
|
||||||
|
const ValidationContext = createContext<Store>();
|
||||||
|
|
||||||
|
export const createValidation = function () {
|
||||||
|
const store = makeStore();
|
||||||
|
const isValid = createEffect(() => {
|
||||||
|
store[1]("valid", Object.keys(store[0].errors).length === 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ContextProvider: FlowComponent = (props) => (
|
||||||
|
<ValidationContext.Provider value={store}>
|
||||||
|
{props.children}
|
||||||
|
</ValidationContext.Provider>
|
||||||
|
);
|
||||||
|
const result = [ContextProvider, store[0]] as [
|
||||||
|
typeof ContextProvider,
|
||||||
|
typeof store[0]
|
||||||
|
];
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EMPTY_OBJECT = {};
|
||||||
|
|
||||||
|
export const validateInput = function ({
|
||||||
|
value: getValue,
|
||||||
|
listen,
|
||||||
|
}: {
|
||||||
|
value?: () => any;
|
||||||
|
listen?: {
|
||||||
|
input?: boolean;
|
||||||
|
blur?: boolean;
|
||||||
|
change?: boolean;
|
||||||
|
observer?: boolean;
|
||||||
|
};
|
||||||
|
} = {}) {
|
||||||
|
// TODO: Default listen values could be set from the ValidationContext
|
||||||
|
listen = listen || { input: true, blur: true, change: false, observer: true };
|
||||||
|
const store = useContext(ValidationContext);
|
||||||
|
const [validity, setValidity] = createSignal<ValidityState>(
|
||||||
|
{ valid: true } as any,
|
||||||
|
{ equals: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
let name = "";
|
||||||
|
let value: any = null;
|
||||||
|
let init = true;
|
||||||
|
let lastBadInput = false;
|
||||||
|
let isCheckbox = false;
|
||||||
|
let el: HTMLInputElement = undefined!;
|
||||||
|
|
||||||
|
const onChange = function () {
|
||||||
|
if (!el) return;
|
||||||
|
let nextValue = isCheckbox ? el.checked : el.value;
|
||||||
|
const validity_ = el.validity || EMPTY_OBJECT;
|
||||||
|
if (
|
||||||
|
init ||
|
||||||
|
nextValue !== value ||
|
||||||
|
// Update state on badInput (value stays unchanged on badInput), but only update the state once
|
||||||
|
validity_.badInput != lastBadInput
|
||||||
|
) {
|
||||||
|
name = name || el.name || el.id;
|
||||||
|
setValidity(validity_);
|
||||||
|
lastBadInput = validity_.badInput;
|
||||||
|
|
||||||
|
if (validity_.valid) {
|
||||||
|
store && store[1]("errors", name, undefined as any);
|
||||||
|
} else {
|
||||||
|
store && store[1]("errors", name, validity_);
|
||||||
|
}
|
||||||
|
|
||||||
|
value = nextValue;
|
||||||
|
store && store[1]("values", name, value === undefined ? null : value);
|
||||||
|
}
|
||||||
|
|
||||||
|
init = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const directive = function (el_: HTMLInputElement) {
|
||||||
|
el = el_;
|
||||||
|
isCheckbox = el.type === "checkbox";
|
||||||
|
|
||||||
|
onCleanup(function () {
|
||||||
|
if (name) {
|
||||||
|
store && store[1]("errors", name, undefined as any);
|
||||||
|
store && store[1]("values", name, undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (getValue) {
|
||||||
|
createEffect(function () {
|
||||||
|
getValue();
|
||||||
|
onChange();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (listen?.observer) {
|
||||||
|
const observer = new MutationObserver(onChange);
|
||||||
|
|
||||||
|
onMount(function () {
|
||||||
|
observer.observe(el, { attributes: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
onCleanup(function () {
|
||||||
|
observer.disconnect();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (listen?.input) {
|
||||||
|
el.addEventListener("input", function () {
|
||||||
|
onChange();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (listen?.change) {
|
||||||
|
el.addEventListener("change", function () {
|
||||||
|
onChange();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (listen?.blur) {
|
||||||
|
el.addEventListener("blur", function () {
|
||||||
|
requestAnimationFrame(onChange);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return [directive, validity, onChange] as [
|
||||||
|
typeof directive,
|
||||||
|
typeof validity,
|
||||||
|
typeof onChange
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
declare module "solid-js" {
|
||||||
|
namespace JSX {
|
||||||
|
interface Directives {
|
||||||
|
validate: boolean;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue