Skip to content

Atom

유틸

Observer

js
export const createObserver = () => {
  const map = new Map();

  return {
    notify: createNotify(map),
    observe: createObserve(map)
  };
};

const createNotify = (map) => (key, value) => {
  const fns = map.get(key);
  fns.forEach(fn => fn(value))
};

const createObserve = (map) => (key, fn) => {
  map.has(key) || map.set(key, new Set());

  const observers = map.get(key);
  observers.add(fn);
};

Ref

js
export const ref = (value) => ({value});

Debounce

js
export const debounce = (callback, ms = 100) => {
  let timer;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => {
      callback(...args)
    }, ms)
  }
};

atom

js
import {createObserver} from './utils/observer.js';
import {ref} from './utils/ref.js';
import {debounce} from './utils/debounce.js';

export const atom = (state) => {
  const KEY = 'atom';
  const observer = createObserver();
  const innerState = ref(state);

  return {
    get: () => innerState.value,
    set: (state) => {
      innerState.value = state;
      observer.notify(KEY)
    },
    observe: (subscriber) => {
      observer.observe(KEY, subscriber);
    }
  }
};

export const useAtom = (atom) => [atom.get(), atom.set];

export const observeAtoms = (atomsObj, subscriber) => {
  const debouncedSubscriber = debounce(subscriber, 1000 / 60);
  Object
    .values(atomsObj)
    .forEach((atom) => {
      atom.observe(debouncedSubscriber)
    })
};

컴포넌트

forEach

js
export const forEach = (obj, fn) => {
  Object
    .entries(obj)
    .forEach(fn)
};

defineComponent

js
import {observeAtoms} from './atom.js';

export const defineComponent = (atomsOrRenderFn, renderFn) => {
  const component = parentNode => {
    if (typeof atomsOrRenderFn === 'function') {
      parentNode.appendChild(atomsOrRenderFn());
      return;
    }

    let dom = renderFn(atomsOrRenderFn);
    parentNode.appendChild(dom);

    observeAtoms(atomsOrRenderFn, () => {
      const newDom = renderFn(atomsOrRenderFn);
      parentNode.replaceChild(newDom, dom);
      dom = newDom
    });
  };

  return component;
};

html

js
import {forEach} from './utils/forEach.js';

export const html = (tagName, children, options) => {
  const elem = document.createElement(tagName);
  appendChild(elem, children);
  appendOptions(elem, options);
  return elem;
};

const appendChild = (elem, children) => {
  if (Array.isArray(children)) {
    children.forEach((child) => {
      typeof child === 'function' ?
        child(elem) :
        elem.appendChild(child)
    })
  } else {
    elem.textContent = children
  }
};

const appendOptions = (elem, options) => {
  if (options) {
    const {attrs = {}, events = {}} = options;

    forEach(attrs, ([key, value]) => {
      elem.setAttribute(key, value)
    });
    forEach(events, ([key, fn]) => {
      elem.addEventListener(key, fn)
    });
  }
};

mount

js
export const mount = (selector, component) => {
  component(document.querySelector(selector));
};

컴포넌트 사용 예제

마운트

js
import {MainComponent} from './advanced/MainComponent.js';
import {mount} from '../core/mount.js';

mount('#app', MainComponent);

Atom

js
import {atom} from '../../core/atom.js';

export const inputAtom = atom('Test');

MainComponent

js
import {html} from '../../core/html.js';
import {defineComponent} from '../../core/defineComponent.js';
import {InputComponent} from './InputComponent.js';
import {TextComponent} from './TextComponent.js';

export const MainComponent = defineComponent(() => {
  return html('div', [
    InputComponent,
    TextComponent,
    TextComponent,
    TextComponent,
    TextComponent,
    TextComponent,
  ])
});

TextComponent

js
import {defineComponent} from '../../core/defineComponent.js';
import {useAtom} from '../../core/atom.js';
import {html} from '../../core/html.js';
import {inputAtom} from './inputAtom.js';

export const TextComponent = defineComponent({inputAtom}, ({inputAtom}) => {
  const [input] = useAtom(inputAtom);

  return html('p', input)
});
  • Atom 변경 시, 다시 랜더링이 필요한 컴포넌트는 이와 같이 사용한다.
    • defineComponent({atomA, atomB, ...}, renderFn)

InputComponent

js
import {defineComponent} from '../../core/defineComponent.js';
import {useAtom} from '../../core/atom.js';
import {html} from '../../core/html.js';
import {inputAtom} from './inputAtom.js';

export const InputComponent = defineComponent(() => {
  const [input, setInput] = useAtom(inputAtom);

  return html('input', [], {
    attrs: {
      value: input,
    },
    events: {
      input: event => setInput(event.target.value)
    }
  })
});

고민

Atom 변경 시, 다시 랜더링되면 Focus와 입력이 종료된다. 그래서 Atom 변경 시, 다시 랜더링이 되지 않게 defineComponent의 첫번째 인자에 atom을 주입 못했다.

다시 랜더링 됬을 때, form 요소가 다시 랜더링 되지 않게 처리가 필요하다.

데모