Creating a library that creates one component is easy. But what makes frameworks like React and Vue shine isn’t because they allow you to put everything into a single component.
What makes them shine is the ability to create children components so you can break your code up into small, manageable pieces.
We’ll do the same too.
Extracting components into separate files
First, we need to extract children components into their respective files. We’ll create the following files:
child.js for the child counter
total-count.js for the total count
We’ll copy-paste the HTML for each of these components into their respective files.
This happens because we’re trying to render the template into an HTML element. But since we did not pass in a selector value, there’s no HTML element to render to.
We can prevent this error by rendering elements if they contain a selector property.
export default function Tiny (options) {
// ...
if (options.selector) {
_render()
_addEventListeners()
}
}
At this point, you should see the parent counter again, but there’s no child counter yet.
Inserting the Child Component via Tiny
We need to insert the child component into the DOM. To do this, our template needs to know what the child component is.
One way of doing this is to pass a custom attribute into the HTML once more. We’ll call this tiny-component.
We’ll set tiny-component to the name of the component we want to use.
We can now get the child component via the components.
export default function Tiny (options) {
// ...
console.log(options.components)
}
The first undefined comes from child.js since we didn’t include any components property. The second comes from main.js, where we see child is a component.
When we create options for users, it’s always a good idea to initialize the option with a default value. In this case, we can set components to an empty object.
There’s quite a bit of code to write to render a child component, so let’s start by creating a _renderChildComponents function.
export default function Tiny (options) {
// ...
function _renderChildComponents () {
}
}
We need to render the parent component first before we can find the child components. We do this by running _renderChildComponents after _render.
export default function Tiny (options) {
// ...
if (options.selector) {
_render()
_addEventListeners()
_renderChildComponents()
}
}
The first thing we need is to find all children components. We can find all children components by searching for the tiny-component attribute.
export default function Tiny (options) {
// ...
function _renderChildComponents () {
const compEls = element.querySelectorAll('[tiny-component]')
}
}
We’ll loop through the Nodelist to get each component placeholder’s element. From there, we can use getAttribute to find the component name.
export default function Tiny (options) {
// ...
function _renderChildComponents () {
const compEls = element.querySelectorAll('[tiny-component]')
for (const compEl of compEls) {
const compName = compEl.getAttribute('tiny-component')
console.log(compName)
}
}
}
The actual component can be found in the components property.
export default function Tiny (options) {
// ...
function _renderChildComponents () {
const compEls = element.querySelectorAll('[tiny-component]')
for (const compEl of compEls) {
const compName = compEl.getAttribute('tiny-component')
const comp = options.components[compName]
console.log(comp)
}
}
}
At this point, the child component is undefined because Tiny doesn’t return anything.
To fix this, we need to return the child component from Tiny, so we can use it on the parent component. We can do this by returning the options object (which essentially contains everything about the component).
We’ll now get information about the child component in the DOM.
To render the child component onto the element, we can set the compEls innerHTML, and voilĂ , we get the child component in the DOM.
export default function Tiny (options) {
// ...
function _renderChildComponents () {
const compEls = element.querySelectorAll('[tiny-component]')
for (const compEl of compEls) {
// ...
compEl.innerHTML = comp.template()
}
}
}
We can reuse the _render function to run this line of code. To do this, we need to pass the element and options into _render. We’ll call the template method in _render.
export default function Tiny (options) {
// ...
function _render (element, options) {
element.innerHTML = options.template()
}
// ...
function _renderChildComponents () {
const compEls = element.querySelectorAll('[tiny-component]')
for (const compEl of compEls) {
// ...
_render(compEl, comp)
}
}
// ...
}
Whenever we call _render, we need to pass in the element and options to use.
export default function Tiny (options) {
// ...
if (options.selector) {
_render(element, options)
_addEventListeners()
_renderChildComponents()
}
}
Adding event listeners to child components
Children components may also have their own event listeners. We can add event listeners by reusing the _addEventListeners function, but we need to modify it to accept the current component’s element and options.
export default function Tiny (options) {
// ...
function _addEventListeners(element, options) {
// ...
}
}
We can pass in the compEl and comp respectively.
export default function Tiny (options) {
// ...
function _renderChildComponents () {
const compEls = element.querySelectorAll('[tiny-component]')
for (const compEl of compEls) {
// ...
_addEventListeners(compEl, comp)
}
}
// ...
}
Since _addEventListeners now requires element and options arguments, we ought to make things explicit when calling _addEventListeners every time.
export default function Tiny (options) {
// ...
if (options.selector) {
_render(element, options)
_addEventListeners(element, options)
_renderChildComponents()
}
}
At this point, you’ll notice an error that says cannot read property bind of undefined.
This error occurs because we haven’t created event handlers in the child component!
For these handlers, we’ll create an increaseCount method instead of increaseChildCount since we’re coding inside the child component.
We’ll also create an increaseParentCount instead of increaseCount to increase the parent’s counter.
If you try to click any button, you’ll run into an error that says cannot read property of template of undefined.
This error occurs because we used _render and _addEventListeners in setState without passing in the component’s element and options.
export default function Tiny (options) {
// ...
options.setState = function (newState) {
// ...
// We didn't pass in element and options
_render()
_addEventListeners()
}
// ...
}
We can get options easily from both parent and child components since options is used directly on setState. But we can’t get the element directly.
To get the component’s element, we can set an element property in options before we use _render.
export default function Tiny (options) {
const element = typeof options.selector === 'string'
? document.querySelector(options.selector)
: options.selector
// Setting element on parent component
options.element = element
// ...
}
export default function Tiny (options) {
function _renderChildComponents () {
const compEls = element.querySelectorAll('[tiny-component]')
for (const compEl of compEls) {
const compName = compEl.getAttribute('tiny-component')
const comp = options.components[compName]
// Setting element on child component
comp.element = compEl
_render(compEl, comp)
_addEventListeners(compEl, comp)
}
}
}
options.element will contain the element now. We can pass this value into _render and _addEventListeners when we’re in setState.
export default function Tiny (options) {
options.setState = function (newState) {
const entries = Object.entries(newState)
for (const entry of entries) {
options.state[entry[0]] = entry[1]
}
_render(options.element, options)
_addEventListeners(options.element, options)
}
}
A tiny refactor
There’s no point passing options.element and options into the same function – options already contains element!
We can refactor _render and _addEventListeners to use the options object instead.
export default function Tiny (options) {
// ...
options.setState = function (newState) {
// ...
_render(options)
_addEventListeners(options)
}
function _renderChildComponents () {
const compEls = element.querySelectorAll('[tiny-component]')
for (const compEl of compEls) {
// ...
_render(comp)
_addEventListeners(comp)
}
}
// ...
if (options.selector) {
_render(options)
_addEventListeners(options)
_renderChildComponents()
}
}
Fixing the Parent Counter
The child counter disappears when you click a button on the parent counter.
This happens because children components are not re-rendered when the component gets re-rendered. The simple fix is to run _renderChildComponents in setState.