UI framework in 5 minutes


    Some time ago I wondered why there are so many UI frameworks for the web? I’ve been in IT for a long time and I don’t remember that UI libraries on other platforms were born and died at the same speed as in WEB. Libraries for desktop OS, such as: MFC, Qt, WPF, etc. - were monsters that developed over the years and did not have a large number of alternatives. Everything is different on the Web - frameworks are released almost every week, leaders change - why is this happening?


    I think the main reason is that the complexity of writing UI libraries has sharply decreased. Yes, to write a library that many will use - it still requires considerable time and expertise, but to write a prototype - which, when wrapped in a convenient API - will be ready for use - it takes very little time. If you are interested in how this can be done, read on.


    Why this article?


    At one time on Habré there was a series of articles - to write X for 30 lines of code on js.


    I thought - is it possible to write a reaction in 30 lines? Yes, for 30 lines I did not succeed, but the final result is quite commensurate with this figure.


    In general, the purpose of the article is purely educational. It can help a little deeper understanding of the principle of the UI framework based on the virtual house. In this article I want to show how pretty simple it is to make another UI Framework based on a virtual home.


    In the beginning I want to say what I mean by the UI framework - because many have different opinions on this. For example, some believe that Angular and Ember is a UI framework and React is just a library that will make it easier to work with the view part of the application.


    We define the UI framework as follows: this is a library that helps to create / update / delete pages or individual page elements in this sense a fairly wide range of wrappers over the DOM API may turn out to be the UI framework, the only question is the abstraction options (API) that this library provides for manipulating the DOM and in the effectiveness of these manipulations


    In the proposed wording - React is quite a UI framework.


    Well, let's see how to write your React with blackjack and more. React is known to use the concept of a virtual home. In a simplified form, it consists in the fact that the nodes of the real DOM are built in strict accordance with the nodes of the previously built virtual DOM tree. Direct manipulation of the real DOM is not welcome, if you need to make changes to the real DOM, the changes are made to the virtual DOM, then the new version of the virtual DOM is compared with the old one, the changes are collected that need to be applied to the real DOM and they are applied in such a way that interaction with the real DOM is minimized DOM - which makes the application more optimal.


    Since the virtual house tree is an ordinary java-script object - it’s quite easy to manipulate it - change / compare its nodes, by the word it’s easy here I understand that the assembly code is virtual but quite simple and can be partially generated by a preprocessor from a declarative language of a higher level JSX.


    Let's start with JSX


    This is an example of JSX code


    const Component = () => (
      
    ) export default Component

    we need to make Componentsuch a virtual DOM created when calling the function


    const vdom = {
      type: 'div',
      props: { className: 'main' },
      children: [
        { type: 'input' },
        {
          type: 'button',
          props: { onClick: () => console.log('yo') },
          children: ['Submit']
        }
      ]
    }

    Of course, we will not write this transformation manually, we will use this plugin , the plugin is outdated, but it is simple enough to help us understand how everything works. It uses jsx-transform , which converts JSX like this:


    jsx.fromString('

    Hello World

    ', { factory: 'h' }); // => 'h("h1", null, ["Hello World"])'

    so, all we need to do is implement the vdom node constructor h, a function that will recursively create virtual DOM nodes in the case of a react, the React.createElement function does this. Below is a primitive implementation of such a function


    export function h(type, props, ...stack) {
      const children = (stack || []).reduce(addChild, [])
      props = props || {}
      return typeof type === "string" ? { type, props, children } : type(props, children)
    }
    function addChild(acc, node) {
      if (Array.isArray(node)) {
        acc = node.reduce(addChild, acc)
      } else if (null == node || true === node || false === node) {
      } else {
        acc.push(typeof node === "number" ? node + "" : node)
      }
      return acc
    }

    Of course, recursion complicates the code a bit here, but I hope it is clear, now with this function we can build vdom


    'h("h1", null, ["Hello World"])' => { type: 'h1', props:null, children:['Hello World']}

    and so for nodes of any nesting


    Great, now our Component function returns the vdom node.


    Now there will be a сложнаяpart, we need to write a function patchthat takes the root DOM element of the application, the old vdom, the new vdom - and updates the nodes of the real DOM in accordance with the new vdom.


    Maybe you can write this code easier, but it turned out so I took the code from the picodom package as a basis


    export function patch(parent, oldNode, newNode) {
      return patchElement(parent, parent.children[0], oldNode, newNode)
    }
    function patchElement(parent, element, oldNode, node, isSVG, nextSibling) {
      if (oldNode == null) {
        element = parent.insertBefore(createElement(node, isSVG), element)
      } else if (node.type != oldNode.type) {
        const oldElement = element
        element = parent.insertBefore(createElement(node, isSVG), oldElement)
        removeElement(parent, oldElement, oldNode)
      } else {
        updateElement(element, oldNode.props, node.props)
        isSVG = isSVG || node.type === "svg"
        let childNodes = []
          ; (element.childNodes || []).forEach(element => childNodes.push(element))
        let oldNodeIdex = 0
        if (node.children && node.children.length > 0) {
          for (var i = 0; i < node.children.length; i++) {
            if (oldNode.children && oldNodeIdex <= oldNode.children.length &&
              (node.children[i].type && node.children[i].type === oldNode.children[oldNodeIdex].type ||
                (!node.children[i].type && node.children[i] === oldNode.children[oldNodeIdex]))
            ) {
              patchElement(element, childNodes[oldNodeIdex], oldNode.children[oldNodeIdex], node.children[i], isSVG)
              oldNodeIdex++
            } else {
              let newChild = element.insertBefore(
                createElement(node.children[i], isSVG),
                childNodes[oldNodeIdex]
              )
              patchElement(element, newChild, {}, node.children[i], isSVG)
            }
          }
        }
        for (var i = oldNodeIdex; i < childNodes.length; i++) {
          removeElement(element, childNodes[i], oldNode.children ? oldNode.children[i] || {} : {})
        }
      }
      return element
    }

    This naive implementation, it is terribly not optimal, does not take into account the identifiers of the elements (key, id) - to correctly update the necessary elements in the lists, but in primitive cases it works fine.


    createElement updateElement removeElementI don’t cite the implementation of the functions here. It’s noticeable; anyone who is interested can see the source here .


    There is the only caveat - when the properties valuefor the inputelements are updated, the comparison should not be done with the old vnode but with the attribute valuein the real house - this will prevent the active element from updating this property (since it is already updated there) and prevent problems with the cursor and selection.


    Well, that’s all now all we have to do is to put these pieces together and write the UI Framework.
    We fit in 5 lines .


    1. As in React, to build the application we need 3
      export function app(selector, view, initProps) {
      selector parameters - the root selector dom into which the application will be mounted (by default 'body')
      view - the function that constructs the root vnode
      initProps - the initial properties of the application
    2. Take the root element in the DOM
      const rootElement = document.querySelector(selector || 'body')
    3. We collect vdom with initial properties
      let node = view(initProps)
    4. We mount the received vdom in the DOM as the old vdom we take null
      patch(rootElement, null, node)
    5. We return the application update function with new properties
      return props => patch(rootElement, node, (node = view(props)))

    Framework is ready!


    'Hello world' on this Framework will look like this:


    import { h, app } from "../src/index"
    function view(state) {
      return (
        

    {`Hello ${state}`}

    render(e.target.value)} />
    ) } const render = app('body', view, 'world')

    This library, like React, supports component composition, adding, removing components at runtime, so that it can be considered the полноценнымUI Framework. A slightly more complex use case can be found here ToDo example .


    Of course, there are a lot of things in this library: life cycle events (although it’s not difficult to fasten them, we ourselves manage the creation / updating / deletion of nodes), separate updates of child nodes like this.setState (for this you need to save links to DOM elements for each vdom node - this will complicate the logic a bit), patchElement code is terribly non-optimal, will not work well on a large number of elements, does not track elements with an identifier, etc.


    In any case, the library was developed for educational purposes - do not use it in production :)


    PS: I was inspired by the magnificent Hyperapp library for this article , part of the code was taken from there.


    Good coding!


    Also popular now: