stianl
4/22/2014 - 7:57 PM

bloop

bloop

(function() {
  // Do not use this library. This is just a fun example to prove a
  // point.
  
  var Bloop = window.Bloop = {};

  var mountId = 0;
  function newMountId() {
    return mountId++;
  }

  Bloop.createClass = function(proto) {  
    function _component(props, children) {
      if(this.constructor !== _component) {
        return new _component(props, children);
      }
      
      var args = _.toArray(arguments);
      if(!(_.isPlainObject(props) && !props._type)) {
        children = props;
        props = null;
        args.unshift(null);
      }

      if(!Array.isArray(children)) {
        children = args.slice(1);
      }

      _.keys(proto).forEach(function(k) {
        var val = proto[k];
        if(_.isFunction(val)) {
          val = val.bind(this);
        }
        this[k] = val;
      }, this);

      this.props = props;
      this.state = this.getInitialState ? this.getInitialState() : null;
      
      if(props && props.state) {
        _.keys(this.state).forEach(function(k) {
          props.state[k] = this.state[k];
        }, this);
        this.state = props.state;
        delete props.state;
      }

      this.children = children;
      this._type = "custom";

      if(this.init) {
        this.init();
      }
    }

    _component.prototype = Object.create(Bloop.createClass.prototype);
    _component.prototype.constructor = _component;
    
    return _component;
  };

  Bloop.renderComponent = function(comp, node) {
    var virtualDOM = comp.render();
    var changed = false;
    
    if(!comp.mountId) {
      comp.mountId = 'rerender-' + newMountId();
      var mount = document.createElement('div');
      mount.id = comp.mountId;
      mount._virtualDOM = virtualDOM;
      mount.appendChild(renderVirtualDOM(virtualDOM));
      node.appendChild(mount);

      if(comp.componentDidRender) {
        comp.componentDidRender();
      }
    }
    else {
      var mount = document.querySelector('#' + comp.mountId);
      var prevDOM = mount._virtualDOM;

      if(serializeVirtualDOM(virtualDOM) !== serializeVirtualDOM(prevDOM)) {
        var dom = renderVirtualDOM(virtualDOM);

        changed = true;
        mount.innerHTML = '';
        mount.appendChild(dom);
        mount._virtualDOM = virtualDOM;

        if(comp.componentDidRender) {
          comp.componentDidRender();
        }
      }
    }

    return changed;
  };

  Bloop.renderComponentToString = function(comp) {
    return renderVirtualDOMToString(comp.render());
  };

  function serializeVirtualDOM(dom) {
    var str = dom._type;
    if(dom.props) {
      _.keys(dom.props).forEach(function(k) {
        var prop = dom.props[k];
        if(_.isFunction(prop)) {
          str += k + 'function';
        }
        else if(k === 'style') {
          str += _.values(dom.props[k]).toString();
        }
        else if(dom.props[k]) {
          str += k + dom.props[k].toString();
        }
      });
    }
    if(dom.children) {
      dom.children.forEach(function(child) {
        if(child) {
          str += child._type ? serializeVirtualDOM(child) : child;
        }
      });
    }
    return str;
  }

  function renderVirtualDOM(comp) {
    if(comp && comp._type) {
      // render native thing
      var node = document.createElement(comp._type);
      var props = comp.props;
      var children = comp.children;
      
      if(props) {
        for(var k in props) {
          if(props.hasOwnProperty(k)) {
            if(k.indexOf('on') === 0) {
              node.addEventListener(k.substring(2).toLowerCase(), props[k]);
            }
            else if(k === 'style') {
              var styles = props[k];
              _.keys(styles).forEach(function(k) {
                node.style[k] = styles[k];
              });
            }
            else {
              node[k] = props[k];
            }
          }
        }
      }

      children.forEach(function(child) {
        if(child != null) {
          node.appendChild(renderVirtualDOM(child));
        }
      });

      return node;
    }
    else {
      return document.createTextNode(comp);
    }
  }

  function renderVirtualDOMToString(comp) {
    if(comp && comp._type) {
      var node = '<' + comp._type;
      var props = comp.props;
      var children = comp.children;
      
      if(props) {
        for(var k in props) {
          if(props.hasOwnProperty(k)) {
            if(k.indexOf('on') !== 0 && k !== 'style') {
              var val = props[k];
              if(k === 'className') {
                k = 'class';
              }
              else {
                k = k.replace(/[A-Z]/g, function(s) { return '-' + s.toLowerCase(); });
              }
              
              node += ' ' + k + '="' + val + '"';
            }
          }
        }
      }

      node += '>';
      
      children.forEach(function(child) {
        if(child != null) {
          node += renderVirtualDOMToString(child);
        }
      });

      node += '</' + comp._type + '>';
      return node;      
    }
    else {
      return comp;
    }
  }

  function DOMCreator(type) {
    return function(props, children) {
      var args = _.toArray(arguments);
      if(!(_.isPlainObject(props) && !props._type)) {
        children = props;
        props = null;
        args.unshift(null);
      }
      
      if(!Array.isArray(children)) {
        children = args.slice(1);
      }
      
      return {
        _type: type,
        props: props,
        children: children.map(function(child) {
          if(child instanceof Bloop.createClass) {
            return child.render();
          }
          return child;
        })
      };
    };
  }

  Bloop.dom = {
    div: DOMCreator('div'),
    span: DOMCreator('span'),
    a: DOMCreator('a'),
    p: DOMCreator('p'),
    em: DOMCreator('em'),
    img: DOMCreator('img'),
    form: DOMCreator('form'),
    button: DOMCreator('button'),
    ul: DOMCreator('ul'),
    li: DOMCreator('li'),
    header: DOMCreator('header'),
    h1: DOMCreator('h1'),
    h2: DOMCreator('h2'),
    h3: DOMCreator('h3'),
    h4: DOMCreator('h4'),
    input: DOMCreator('input'),
    textarea: DOMCreator('textarea')
  };
})();