import { createEvent, createEffect, createStore, createApi, sample } from "effector";import { useList, useUnit } from "effector-react";
const submitForm = createEvent();const addMessage = createEvent();const changeFieldType = createEvent();
const showTooltipFx = createEffect(() => new Promise((rs) => setTimeout(rs, 1500)));
const saveFormFx = createEffect((data) => { localStorage.setItem("form_state/2", JSON.stringify(data, null, 2));});const loadFormFx = createEffect(() => { return JSON.parse(localStorage.getItem("form_state/2"));});
const $fieldType = createStore("text");const $message = createStore("done");const $mainForm = createStore({});const $types = createStore({ username: "text", email: "text", password: "text",});
const $fields = $types.map((state) => Object.keys(state));
$message.on(addMessage, (_, message) => message);
$mainForm.on(loadFormFx.doneData, (form, result) => { let changed = false;
form = { ...form }; for (const key in result) { const { value } = result[key]; if (value == null) continue; if (form[key] === value) continue; changed = true; form[key] = value; } if (!changed) return;
return form;});
const mainFormApi = createApi($mainForm, { upsertField(form, name) { if (name in form) return;
return { ...form, [name]: "" }; }, changeField(form, [name, value]) { if (form[name] === value) return;
return { ...form, [name]: value }; }, addField(form, [name, value = ""]) { if (form[name] === value) return;
return { ...form, [name]: value }; }, deleteField(form, name) { if (!(name in form)) return; form = { ...form }; delete form[name];
return form; },});
$types.on(mainFormApi.addField, (state, [name, value, type]) => { if (state[name] === type) return;
return { ...state, [name]: value };});$types.on(mainFormApi.deleteField, (state, name) => { if (!(name in state)) return; state = { ...state }; delete state[name];
return state;});$types.on(loadFormFx.doneData, (state, result) => { let changed = false;
state = { ...state }; for (const key in result) { const { type } = result[key];
if (type == null) continue; if (state[key] === type) continue; changed = true; state[key] = type; } if (!changed) return;
return state;});
const changeFieldInput = mainFormApi.changeField.prepend((e) => [ e.currentTarget.name, e.currentTarget.type === "checkbox" ? e.currentTarget.checked : e.currentTarget.value,]);
const submitField = mainFormApi.addField.prepend((e) => [ e.currentTarget.fieldname.value, e.currentTarget.fieldtype.value === "checkbox" ? e.currentTarget.fieldvalue.checked : e.currentTarget.fieldvalue.value, e.currentTarget.fieldtype.value,]);
const submitRemoveField = mainFormApi.deleteField.prepend((e) => e.currentTarget.field.value);
$fieldType.on(changeFieldType, (_, e) => e.currentTarget.value);$fieldType.reset(submitField);
submitForm.watch((e) => { e.preventDefault();});submitField.watch((e) => { e.preventDefault(); e.currentTarget.reset();});submitRemoveField.watch((e) => { e.preventDefault();});
sample({ clock: [submitForm, submitField, submitRemoveField], source: { values: $mainForm, types: $types }, target: saveFormFx, fn({ values, types }) { const form = {};
for (const [key, value] of Object.entries(values)) { form[key] = { value, type: types[key], }; }
return form; },});
sample({ clock: addMessage, target: showTooltipFx,});sample({ clock: submitField, fn: () => "added", target: addMessage,});sample({ clock: submitRemoveField, fn: () => "removed", target: addMessage,});sample({ clock: submitForm, fn: () => "saved", target: addMessage,});
loadFormFx.finally.watch(() => { ReactDOM.render(<App />, document.getElementById("root"));});
function useFormField(name) { const type = useStoreMap({ store: $types, keys: [name], fn(state, [field]) { if (field in state) return state[field];
return "text"; }, }); const value = useStoreMap({ store: $mainForm, keys: [name], fn(state, [field]) { if (field in state) return state[field];
return ""; }, }); mainFormApi.upsertField(name);
return [value, type];}
function Form() { const pending = useUnit(saveFormFx.pending);
return ( <form onSubmit={submitForm} data-form autocomplete="off"> <header> <h4>Form</h4> </header> {useList($fields, (name) => ( <InputField name={name} /> ))}
<input type="submit" value="save form" disabled={pending} /> </form> );}
function InputField({ name }) { const [value, type] = useFormField(name); let input = null;
switch (type) { case "checkbox": input = ( <input id={name} name={name} value={name} checked={value} onChange={changeFieldInput} type="checkbox" /> ); break; case "text": default: input = <input id={name} name={name} value={value} onChange={changeFieldInput} type="text" />; }
return ( <> <label htmlFor={name} style={{ display: "block" }}> <strong>{name}</strong> </label> {input} </> );}
function FieldForm() { const currentFieldType = useUnit($fieldType); const fieldValue = currentFieldType === "checkbox" ? ( <input id="fieldvalue" name="fieldvalue" type="checkbox" /> ) : ( <input id="fieldvalue" name="fieldvalue" type="text" defaultValue="" /> );
return ( <form onSubmit={submitField} autocomplete="off" data-form> <header> <h4>Insert new field</h4> </header> <label htmlFor="fieldname"> <strong>name</strong> </label> <input id="fieldname" name="fieldname" type="text" required defaultValue="" /> <label htmlFor="fieldvalue"> <strong>value</strong> </label> {fieldValue} <label htmlFor="fieldtype"> <strong>type</strong> </label> <select id="fieldtype" name="fieldtype" onChange={changeFieldType}> <option value="text">text</option> <option value="checkbox">checkbox</option> </select> <input type="submit" value="insert" /> </form> );}
function RemoveFieldForm() { return ( <form onSubmit={submitRemoveField} data-form> <header> <h4>Remove field</h4> </header> <label htmlFor="field"> <strong>name</strong> </label> <select id="field" name="field" required> {useList($fields, (name) => ( <option value={name}>{name}</option> ))} </select> <input type="submit" value="remove" /> </form> );}
const Tooltip = () => { const [visible, text] = useUnit([showTooltipFx.pending, $message]);
return <span data-tooltip={text} data-visible={visible} />;};
const App = () => ( <> <Tooltip /> <div id="app"> <Form /> <FieldForm /> <RemoveFieldForm /> </div> </>);
await loadFormFx();
css` [data-tooltip]:before { display: block; background: white; width: min-content; content: attr(data-tooltip); position: sticky; top: 0; left: 50%; color: darkgreen; font-family: sans-serif; font-weight: 800; font-size: 20px; padding: 5px 5px; transition: transform 100ms ease-out; }
[data-tooltip][data-visible="true"]:before { transform: translate(0px, 0.5em); }
[data-tooltip][data-visible="false"]:before { transform: translate(0px, -2em); }
[data-form] { display: contents; }
[data-form] > header { grid-column: 1 / span 2; }
[data-form] > header > h4 { margin-block-end: 0; }
[data-form] label { grid-column: 1; justify-self: end; }
[data-form] input:not([type="submit"]), [data-form] select { grid-column: 2; }
[data-form] input[type="submit"] { grid-column: 2; justify-self: end; width: fit-content; }
#app { width: min-content; display: grid; grid-column-gap: 5px; grid-row-gap: 8px; grid-template-columns: repeat(2, 3fr); }`;
function css(tags, ...attrs) { const value = style(tags, ...attrs); const node = document.createElement("style"); node.id = "insertedStyle"; node.appendChild(document.createTextNode(value)); const sheet = document.getElementById("insertedStyle");
if (sheet) { sheet.disabled = true; sheet.parentNode.removeChild(sheet); } document.head.appendChild(node);
function style(tags, ...attrs) { if (tags.length === 0) return ""; let result = " " + tags[0];
for (let i = 0; i < attrs.length; i++) { result += attrs[i]; result += tags[i + 1]; }
return result; }}
Contributors