function el( tagName: K, props?: Partial & { ref?: (r: HTMLElementTagNameMap[K]) => void; }, ...children: Array ): HTMLElementTagNameMap[K] { const element = document.createElement(tagName); Object.assign(element, props); children.filter(Boolean).forEach((child) => { if (child instanceof HTMLElement) { element.appendChild(child); } else { const textNode = document.createTextNode(child); element.appendChild(textNode); } }); if (props?.ref) { props.ref(element); } return element; } // Returns a render function. Each time render is run the old element is replaced // with the new one in the DOM. function DOMUpdater(Component: () => T): () => T { let element: T | undefined; return () => { const newElement = Component(); element?.replaceWith(newElement); element = newElement; return newElement; }; } // App Component function App() { let toDos: string[] = []; const onSubmit = (todo: string) => { // Add to do toDos.push(todo); render(); }; const onRemove = (index: number) => { // Remove to do toDos.splice(index, 1); render(); }; const render = DOMUpdater(() => el( 'div', { className: 'App' }, Form({ onSubmit }), List({ items: toDos, onRemove }), toDos.length > 0 && el('div', null, 'you have ', toDos.length.toString(), ' to dos') ) ); return render(); } // Form Component interface FormProps { onSubmit: (item: string) => void; } function Form(props: FormProps) { let elInput: HTMLInputElement; const onsubmit = (event: Event) => { event.preventDefault(); props.onSubmit(elInput.value); elInput.value = ''; }; return el( 'form', { onsubmit }, el('input', { ref: (element) => (elInput = element) }), el('input', { type: 'submit' }) ); } // List Component interface ListProps { items: string[]; onRemove?: (index: number) => void; } function List({ items, onRemove }: ListProps) { const elItems = items.map((item, index) => el('li', null, item, el('button', { onclick: () => onRemove(index) }, 'x')) ); return el('ol', null, ...elItems); } document.body.appendChild(App());