Part One, Basic Component

    Components will be the basic building blocks of our frontend framework. Components will allow modular composition and encapsulation. The first component implementation will be rather naive, but that’s alright since its implementation will evolve over time. Let’s start with creating a Component class:

    const COOL_COMPONENT = Symbol("component");
    
    class Component {
      constructor(element, args = {}) {
        this.$$typeof = COOL_COMPONENT;
        this.el = document.createElement(element);
        this.args = args;
      }
    }
    

    Breaking this down:

    const COOL_COMPONENT = Symbol('component')

    This creates a unique representation for our component type, and will help identify component instances as they get passed around our framework. The component takes an element type and some arguments which we’ll handle a bit later. The constructor also creates a new element which we’ll mount on the DOM with a mount function. Now that we have a very basic component, we need a way to mount it to the DOM. Starting with a naive implementation, we can do something like this:

    function renderToDOM(id, component) {
      const root = document.querySelector(`#${id}`);
      root.appendChild(Component.el);
    }
    

    With this code, we can create components and mount them to the DOM. Let’s start building!

    <div id="root"></div>
    <script>
      const div = new Component("div");
      renderToDOM("root", div);
    </script>
    

    This is quite boring, so let’s add the ability to style our components and render sub components, or children. To do this, we will add a couple more methods to our Component class. We’ll start with the mount method:

    // inside Component class
    mount() {
      Object.entries(this.args.style).forEach(([styleKey, styleValue]) => {
        this.el.style[styleKey] = styleValue
      })
    }
    

    In the mount method, we take a style object passed to the component in args and append the component’s element with the given style property. While this currently doesn’t allow us to style components with css selectors, it does allow for simple inline styling that will help us troubleshoot components. For our component to render children, we need to add a render method which will take children components as an argument and correctly render them out to the DOM.

    // inside of Component class
    render(children) {
      if (!Array.isArray(children)) {
        throw new Error('Children must be an array!')
      }
      const components = []
      children.forEach(child => {
        if (child.$$typeof === COOL_COMPONENT) {
          components.push(child.el)
        }
        if (typeof child === 'string') {
          this.el.appendChild(document.createTextNode(child))
        }
    
        this.el.append(...components)
    
        return this
      })
    }
    

    When given an array of children, render iterates through the array and checks if the child is a component. If it is, then it adds it to another array of components to be appended, if the child is a string, it is appended as a text node to the current component.

    Putting all these pieces together looks like this:

    const COOL_COMPONENT = Symbol("component");
    
    class Component {
      constructor(element, args = {}) {
        this.$$typeof = COOL_COMPONENT;
        this.el = document.createElement(element);
        this.args = args;
      }
      mount() {
        Object.entries(this.args.style).forEach(([styleKey, styleValue]) => {
          this.el.style[styleKey] = styleValue;
        });
      }
    
      render(children) {
        if (!Array.isArray(children)) {
          throw new Error("Children must be an array!");
        }
        const components = [];
        children.forEach((child) => {
          if (child.$$typeof === COOL_COMPONENT) {
            components.push(child.el);
          }
          if (typeof child === "string") {
            this.el.appendChild(document.createTextNode(child));
          }
    
          this.el.append(...components);
    
          return this;
        });
      }
    }
    
    <!--- index.html ---->
    <div id="root"></div>
    <script>
      const div = new Component("div", {
        style: { height: "300px", width: "300px", background: "papayawhip" },
      });
    
      const title = new Component("h1");
    
      div.render([title.render(["This is a title"])]);
    
      renderToDOM("root", div);
    </script>
    

    One last thing to do for our basic component is to add event listeners. We can do this in the mount method of our comonent:

    
    mount() {
      Object.entries(this.args.style).forEach(([styleKey, styleValue]) => {
        this.el.style[styleKey] = styleValue
      })
      if (this.args.on) {
        this.args.on.forEach(handler => {
          const [event, func] = Object.entries(handler)[0]
          this.el.addEventListener(event, func)
        })
      }
    }
    

    Now we can pass an array of events and their respective handler functions to our component with the on argument.

    const div = new Component("div", {
      style: { height: "300px", width: "300px", background: "papayawhip" },
      on: [{ click: () => console.log("Hello there") }],
    });
    

    With that, we have a basic component that can be styled and have event listeners attached to it. Next time we’ll continue iterating on this component to improve the api and extending it to handle things like class names and asynchronous actions.

    See Also

    Mentioned around the web