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