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