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 요소가 다시 랜더링 되지 않게 처리가 필요하다.

데모 ​