Drawer + Form
Single-store validation, async promo code check, submit state management, and a success panel — all with Objs.
Architecture
formStore = o.createStore({ values, errors, touched, status, promoStatus, discount }) formStore.subscribe(nameField, 'sync') → error / ok styles on this field only formStore.subscribe(emailField, 'sync') → error / ok styles on this field only formStore.subscribe(messageField, 'sync') → error / ok styles on this field only formStore.subscribe(promoField, 'sync') → checking / valid / invalid badge formStore.subscribe(submitBtn, 'sync') → disabled / loading / ready state formStore.subscribe(form, 'sync') → idle form ↔ success panel One store.notify() → six components update only their own DOM nodes. Promo validation is async — store sets promoStatus: 'checking', blocks submit until resolved.
Key patterns
Field atom —
refs + granular sync// Each field accesses its own sub-elements via refs — no selectors sync: ({ self, errors, touched }) => { const { input, error: errorEl } = self.refs; // ObjsInstances, not raw DOM if (errors[fieldName]) { input.addClass('field__input--error').removeClass('field__input--ok'); errorEl.html(errors[fieldName]); } else { input.addClass('field__input--ok').removeClass('field__input--error'); errorEl.html(''); } } // formStore.set('email', val) → notify() → only emailField.sync() changes the DOM
Async promo check — store as gatekeeper
// Inside formStore — async method sets status, calls notify() twice async checkPromo(code) { this.promoStatus = 'checking'; this.notify(); // → submitBtn.sync sees 'checking', disables itself await validateOnServer(code); this.promoStatus = 'valid'; this.discount = 20; this.notify(); // → promoField.sync shows badge, submitBtn re-enables }, isValid() { return fieldsOk && this.promoStatus !== 'checking'; // submit gate }
Store methods over raw event handlers
// Wiring — field knows nothing about other fields input.on('input', (e) => formStore.set('email', e.target.value)); input.on('blur', () => formStore.blur('email')); // formStore.blur() marks touched, runs validator, calls notify() // Every subscriber decides what to do — submit button re-checks isValid()
Why Objs
React
// One state object rerenders the whole form
const [form, setForm] = useState({
values: {}, errors: {}, touched: {},
status: 'idle', promoStatus: 'idle'
});
// Every keystroke triggers a full form rerender
// All fields reconciled even when only one changed
// Async promo needs useEffect + cleanup:
useEffect(() => {
if (form.values.promo.length !== 4) return;
const id = setTimeout(async () => {
setForm(f => ({...f, promoStatus:'checking'}));
const res = await checkPromo(form.values.promo);
setForm(f => ({...f, promoStatus: res ? 'valid' : 'invalid'}));
}, 500);
return () => clearTimeout(id);
}, [form.values.promo]);
Objs
// One store, six independent subscribers
const formStore = o.createStore({
values: {}, errors: {}, touched: {},
status: 'idle', promoStatus: 'idle',
// Async built directly into store method
async checkPromo(code) {
this.promoStatus = 'checking';
this.notify(); // submit disables instantly
const res = await check(code);
this.promoStatus = res ? 'valid' : 'invalid';
this.notify(); // each field updates itself
}
});
// Typing in email field: only emailField.sync() runs
// Promo checking: only promoField.sync() + submitBtn.sync()