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()

objs-core on npm