Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consider removing Proxy for creating functional component notation (div.. button... etc) #33

Open
SilentCicero opened this issue Mar 23, 2017 · 8 comments

Comments

@SilentCicero
Copy link

I understand we want components to be written like button( or div(.. But if we are aiming for size.. just strict policy H('div',... H('button'.... would do just fine. Considering people use these things with transpires and libs like hyperx anyway, it's less of a concern to actually functionally notate button or div... if people want this, they can just write a small wrapper around it.. but it shouldn't be in the base engine Very few actually write by hand functional notation of the elements. This will save you a lot of bytes for doing other more important things. The className thing seems to be pretty important, so I understand the needs there.

Anyway, just a thought.

@styfle
Copy link
Contributor

styfle commented Mar 23, 2017

I believe these were removed in the latest verison.

@SilentCicero
Copy link
Author

@styfle still seems to be in the latest commit:

  /**
   * Expose as `H` a proxy around the createElement function that can either be used
   * either as a function (ex. `H('div')`, or as a proxied method `H.div()` for creating
   * virtual DOM elements.
   */
  window.H = new Proxy(
    createElement,
    {
      get: (targetFn, tagName) =>
        targetFn[tagName] ||                                          // Make sure we don't override any native
                                                                      // property or method from the base function

        wrapClassProxy(                                               // Otherwise, for every tag we extract a
          createElement.bind(global, tagName)                         // class-wrapped crateElement method, bound to the
        )                                                             // tag named as the property requested.
    }
  )

@styfle
Copy link
Contributor

styfle commented Mar 23, 2017

Oh you're right. It seems like it could be removed if it makes it smaller.

Using H.div() seems unnecessary. It's only one more character to type H('div') but most people will probably use a compiler.

/cc @wavesoft

@SilentCicero
Copy link
Author

/**
 * .dom - A Tiny VDom Template Engine
 *
 * Copyright 2017 Ioannis Charalampidis (wavesoft)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/* BEGIN NPM-GLUE */

// This code block will be striped when building the stand-alone version.
// When using `npm` this exports the correct functions in order to be easily
// imported in the correct scope, without leaking to the global scope.

var window = typeof window !== "undefined" && window || {};
module.exports = window;

/* END NPM-GLUE */

// ((global) => {

  /**
   * Create a VNode element
   *
   * @param {String|Function} element - The tag name or the component to render
   * @param {Object} [props] - The object properties
   * @param {Array} [children] - The child VNode elements
   * @returns {VNode} Returns a virtual DOM instance
   */
  let createElement = (element, props={}, ...children) => (
    element = ({                                                      // The reference of `element` will be kept
                                                                      // in the object so we are safe to replace it

      $: element,                                                     // 'E' holds the name or function passed as
                                                                      // first argument

      a: (props.$ || props.trim || props.map)                         // If the props argument is a renderable VNode,
                                                                      // a string or an array, then

          ? {c: [].concat(props, ...children)}                        // ... prepend it to the children
          : (props.c = [].concat(...children)) && props               // ... otherwise append 'C' to the property
                                                                      // the .concat ensures that arrays of children
                                                                      // will be flattened into a single array.

      }
    ),

    element.a.className = element.a.className || '',                  // Ensure `className` always exist, since this
                                                                      // way we can cleanly replace className when
                                                                      // removed.
    element
  )

  /**
   * Render a VNode in the DOM
   *
   * @param {VNode|Array<VNode>} vnodes - The node on an array of nodes to render
   * @param {HTLDomElement}
   */
  let render = window.R = (
    vnodes,                                                           // 1. The vnode tree to render
    dom,                                                              // 2. The DOMElement where to render into

    _npath,                                                           // a. The current state path
    _children=dom.childNodes,                                         // b. Shorthand for accessing the children
    _c=0                                                              // c. Counter for processed children

  ) => {

    (vnodes.map ? vnodes : [vnodes]).map(                             // Cast `vnodes` to array if nor already

                                                                      // In this `map` loop we ensure that the DOM
                                                                      // elements correspond to the correct virtual
                                                                      // node elements.
      (
        vnode,                                                        // 1. We handle the vnode from the array
        index,                                                        // 2. And the index

        _unused1,                                                     // We don't handle the array, but we need the
                                                                      // placeholder for the local variables after

        _path=_npath+' '+index,                                       // a. The state path of this vnode
        _path_state=wrapClassProxy[_path] || [{}, vnode.$],           // b. Get the state record for this path
        _state=(                                                      // c. Update and get the state record
          wrapClassProxy[_path] =                                     //    The record is an the following format:
            _path_state[1] != vnode.$                                 //  [ {state object},
            ? [{}, vnode.$]                                           //    'vnode element' ]
            : _path_state                                             //    The second component is needed in order to
        ),                                                            //    reset the state if the component has changed
        _child=_children[_c++],                                       // d. Get the next DOM child + increment counter
        _hooks={a:vnode.$},
        _new_dom                                                      // e. The new DOM element placeholder

      ) => {

        /* Expand functional Components */

        (vnode.$ || _unused1).call &&                                 // (This expands to : vnode.$ && vnode.$.call &&)
                                                                      // If the vnode is a functional component, expand
          (vnode = vnode.$(                                           // it and replace the current vnode variable.

            vnode.a,                                                  // 1. The component properties
            { state: _state[0],                                                // 2. The stateful component state

            setState: (newState) =>                                             // 3. The setState function

              Object.assign(                                          // First we update the state part of the record
                _state[0],                                            // Note: When we defined the variable we kept the
                newState                                              //       reference to the record array
              ) &&

              render(                                                 // We then trigger the same render cycle that will
                vnodes,                                               // update the DOM
                dom,
                _npath
              ),

              hooks: _hooks }

          ));

        /* Create new DOM element */

        _new_dom =                                                    // We prepare the new DOM element in advance in
          vnode.trim                                                  // order to spare a few comparison bytes
            ? document.createTextNode(vnode)
            : document.createElement(vnode.$);

        /* Keep or replace the previous DOM element */

        _new_dom =
          _child                                                      // If we have a previous child we first check if
            ? (_child.$ != vnode.$ && _child.data != vnode)           // the VNode element or the text are the same

              ? (
                  dom.replaceChild(                                   // - If not, we replace the old element with the
                    _new_dom,                                         //   new one.
                    _child
                  ),
                  _new_dom                                            //   ... and we make sure we return the new DOM
                )
              : _child                                                // - If it's the same, we keep the old child

            : dom.appendChild(                                        // mount lifecycle method and append
                _new_dom
              );

        /* Call lifecycle methods */

        (
          (
            _child
              ? _child.a != _hooks.a
                ? ((_child.u || createElement)(), _hooks.m)
                : _hooks.d
              : _hooks.m
          ) || createElement
        )(_new_dom, _child);

        /* Update Element */

        Object.assign(_new_dom, vnode, _hooks);                       // Keep the following information in the DOM:
                                                                      // - $ : The tag name from the vnode. We use this
                                                                      //       instead of the .tagName because some
                                                                      //       browsers convert it to capital-case
                                                                      // - u : The `didUnmount` hook that is called when
                                                                      //       the DOM element is removed
                                                                      //
                                                                      // By assigning the entire _hooks and vnode
                                                                      // objects we expose some unneeded properties, but
                                                                      // it occupies less space than assigning $ and u
                                                                      // individually.

        vnode.trim
          ? _new_dom.data = vnode                                     // - String nodes update only the text
          : Object.keys(vnode.a).map(                                 // - Element nodes have properties
              (
                key                                                   // 1. The property name
              ) =>

                key == 'style' ?                                      // The 'style' property is an object and must be
                                                                      // applied recursively.
                  Object.assign(
                    _new_dom[key],                                    // '[key]' is shorter than '.style'
                    vnode.a[key]
                  )

                : (_new_dom[key] !== vnode.a[key] &&                  // All properties are applied directly to DOM, as
                  (_new_dom[key] = vnode.a[key]))                     // long as they are different than ther value in the
                                                                      // instance. This includes `onXXX` event handlers.

            ) &&
            render(                                                   // Only if we have an element (and not  text node)
              vnode.a.c,                                              // we recursively continue rendering into it's
              _new_dom,                                               // child nodes.
              _path
            )
      }
    );

    /* Remove extraneous nodes */

    while (_children[_c]) {                                           // The _c property keeps track of the number of
                                                                      // elements in the VDom. If there are more child
                                                                      // nodes in the DOM, we remove them.

      (_children[_c].u || createElement)();                           // We then call the unmount lifecycle method for the
                                                                      // elements that will be removed

      render(                                                         // Remove child an trigger a recursive child removal
        [],                                                           // in order to call the correct lifecycle methods in our
        dom.removeChild(_children[_c])                                // deep children too.
      )
    }

  }

  /**
   * Helper function that wraps an element shorthand function with a proxy
   * that can be used to append class names to the instance.
   *
   * The result is wrapped with the same function, creating a chainable mechanism
   * for appending classes.
   *
   * @param {function} factoryFn - The factory function to call for creating vnode
   */
  let wrapClassProxy = (factoryFn) =>
    new Proxy(                                                        // We are creating a proxy object for every tag in
                                                                      // order to be able to customize the class name
                                                                      // via a shorthand call.
      factoryFn,
      {
        get: (targetFn, className, _instance) =>
          wrapClassProxy(
            (...args) => (
              (_instance=targetFn(...args))                           // We first create the Virtual DOM instance by
                                                                      // calling the wrapped factory function

                .a.className += ' ' + className,                      // And then we assign the class name,
                                                                      // concatenating to the previous value

              _instance                                               // And finally we return the instance
            )
          )
      }
    )

  /**
   * Expose as `H` a proxy around the createElement function that can either be used
   * either as a function (ex. `H('div')`, or as a proxied method `H.div()` for creating
   * virtual DOM elements.
   */
  window.H = createElement;

// })(window);

New dot-dom, with different notation and minus the proxy method.

@wavesoft
Copy link
Owner

wavesoft commented Mar 25, 2017

Hey guys, glad to see that you are creating tiny forks that fits in the comments 😄
@SilentCicero you can find the non-proxy version in the history. It was one of my first implementations, but I had some issues with minification, and that's why I introduced the Proxy approach 😉

This example has some repeating statements, but they cannot really be minified further:

R(
  H('div', 
    H('span', 'Hello '),
    H('strong', 'world '),
    H('span', 'how are you '),
    H('span', 'doing '),
    H('strong', 'today?')
  ),
  document.body
)

However this, has more specific variable reference and it can help minifiers reduce the final size even further:

const {div, span, strong} = H;
R(
  div(
    span('Hello '),
    strong('world '),
    span('how are you '),
    span('doing '),
    strong('today?')
  )
)

@wavesoft
Copy link
Owner

wavesoft commented Mar 25, 2017

Ps. Don't forget the awesome className shorthand you can get with the proxy 😉 :

R(div.primary.otherclass('hello'), document.body);

@SilentCicero
Copy link
Author

@wavesoft okay awesome. Well, I see your point now. I think I can just do a wrapper anyway for my notation.

@SilentCicero
Copy link
Author

SilentCicero commented Mar 28, 2017

well, you could also write all the 'div' strings in a constant for example. That would solve that problem.

div(...)

and h(DIV, ...) is a two byte difference just about (i.e the "H" and the ",").

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants