tunnckoCore / ideas

:notebook: My centralized place for ideas, thoughts and todos. Raw, PoC implementations and so on... :star:
http://j.mp/1stW47C
6 stars 0 forks source link

mich - (Virtual?) DOM Builder, parser, renderer and diffing - microscopic "framework" #71

Open tunnckoCore opened 7 years ago

tunnckoCore commented 7 years ago

Built on HAST - Hypertext Abstract Syntax Tree format.

mich-parse-selector ~250 bytes gzip+min

Parses simple (id, class, tag name) string to a HAST node

var parse = require('mich-parse-selector')

// if tag name not given, defaults to `div`
var node = parse('div.foo#page.bar.quxie-sea.btn')

console.log(node)
// node.properties.className: [ 'foo', 'bar', 'quxie-sea', 'btn']
// node.properties.id: 'page'

mich-h ~500 bytes gzip+min

Uses -parse-selector and in addition handles properties/attributes and childrens so it can be used as hyperscript or any other h implementation, finally it gives you AST - specifically HAST tree of object nodes. It is fully compatible with hyperscript

var h = require('mich-h')
var tree = h('div#page',
  h('#header', // if tag name is not given, defaults to `div`
    h('h1.classy', { style: {'background-color': '#333'} })),
  h('nav#menu', { style: {'background': '#2f2'} },
    h('ul',
      h('li', 'one'),
      h('li', 'two'),
      h('li', 'three'))),
  h('h2#title', 'content title',  { style: {'background-color': 'red'} }),
  h('p.first', // classes of that `p` would be `first, foobie, quxie`
    { className: 'foobie' },
    "so it's just like a templating engine,\n",
    { class: 'quxie' },
    "but easy to use inline with javascript\n",
    { onclick: () => {} }
  ),
  h('p',
    "the intention is for this to be used to create\n",
    h('strong', 'charlike', { style: 'background: white; color: green' }),
    " reusable, interactive html widgets. "))

console.log(tree)

mich-to-html ~420 bytes

Gets an AST (HAST) tree and compiles it to html string, perfect for server-side rendering (SSR), excluding the events

var toHtml = require('mich-to-html')
var htmlString = toHtml(tree)

console.log(htmlString)

outputs

<div id="page">
  <div id="header">
    <h1 class="classy" style="background-color: #333;"></h1>
  </div>
  <nav id="menu" style="background: #2f2;">
    <ul>
      <li>one</li>
      <li>two</li>
      <li>three</li>
    </ul>
  </nav>
  <h2 id="title" style="background-color: red;">content title</h2>
  <p class="foobie">so it's just like a templating engine,
  but easy to use inline with javascript</p>
  <p>the intention is for this to be used to create <strong>charlike</strong> reusable,
  interactive html widgets.</p>
</div>

mich-to-dom

Gets an AST (HAST) and creates real DOM tree from it

mich-morph - based on Facebook Reconciliation algorithm

not sure for the name, but ... gets 2 AST trees and makes a diffing then patch

mich

Gets a tree, makes a diff / patch (apply plugins), renders to dom/html

tunnckoCore commented 7 years ago

Woohoo, JSX! :rofl:

/** @jsx h */
const h = require('mich-h')

const tree = <div id="page">
  <div id="header">
    <h1 class="classy" style="background-color: #333; color: purple"></h1>
  </div>
  <nav id="menu" style="background: #2f2;">
    <ul>
      <li>one</li>
      <li class="sec">two</li>
      <li>three</li>
    </ul>
  </nav>
  <h2 id="title" style="background-color: red;">content title</h2>
  <p class="first foobie quxie">so it's just like a templating engine,
  but easy to use inline with javascript</p>
  <p>the intention is for this to be usedto create <strong>charlike</strong>
  reusable, interactive html widgets.</p>
</div>

console.log(tree)
tunnckoCore commented 7 years ago

mich-to-html

Very small and naive rendering of HAST to HTML string

module.exports = function toHtml (tree) {
  var buf = ''
  traverse([].concat(tree), function (node) {
    if (node.type === 'text') {
      buf += node.value
      return
    }
    if (node.type !== 'element') return

    buf = `${buf}<${node.tagName}${toAttrs(node.properties)}>`
    buf = `${buf}${toHtml(node.children)}</${node.tagName}>\n`
  })

  return buf
}

function traverse (tree, cb) {
    if (Array.isArray(tree)) {
      tree.forEach(function (node) {
        traverse(cb(node), cb)
      })
    } else if (typeof tree === 'object' && tree.hasOwnProperty('children')) {
      traverse(tree.children, cb)
    }
    return tree
}

var transform = {
  'className': 'class',
  'htmlFor': 'for',
  'httpEquiv': 'http-equiv'
}

function toAttrs (props) {
  var attr = ''

  for (var prop in props) {
    var value = props[prop]
    prop = prop in transform ? transform[prop] : prop
    value = prop === 'class' ? value.join(' ') : value
    var type = typeof value

    if (value === true) {
      attr += ' ' + prop
    } else if (value === false) {
      attr += ' ' + prop + '="false"'
    } else if (type === 'string' || type === 'number') {
      attr += ' ' + prop + '="' + value + '"'
    } else if (prop === 'style') {
      var css = ''
      for (var key in value) {
        css += key + ': ' + value[key] + ';'
       }
      attr += ' ' + prop + '="' + css + '"'
    } else if (prop === 'dataset') {
      for (var key in value) {
        attr += ' data-' + key + '="' + value[key] + '"'
      }
    }
  }
  return attr
}

Support for data-, class, style, for, http-equiv, truthy and falsey attributes

tunnckoCore commented 7 years ago

Example

var ast = h('div#page.foo.bar.qux', { className: 'ok' },
  h('#header', // if tag name is not given, defaults to `div`
    h('h1.classy', 'hi', { style: 'background-color: #333; color: purple' })),
  h('nav#menu', { style: {'background': '#2f2', 'font-size': '12px' } },
    h('ul',
      h('li', 'one', { dataset: { foo: 'bar', 'set': 'ok' } }),
      h('li.sec', 'two'),
      h('li', { download: true }, 'three'))),
  h('h2#title', 'content title',  { style: {'background-color': 'red'} }),
  h('p.first', // classes of that `p` would be `first, foobie`
    { className: 'foobie' },
    "so it's just like a templating engine,\n",
    "but easy to use inline with javascript\n",
    { onclick: () => {} }
  ),
  h('p',
    "the intention is for this to be used to create\n",
    h('strong', 'charlike', { style: 'background: white; color: green' }),
    " reusable, interactive html widgets. "))

var res = toHtml(ast)
console.log(res)

outputs a HTML string

<div id="page" class="foo bar qux ok">
  <div id="header">
    <h1 class="classy" style="background-color: #333; color: purple">hi</h1>
  </div>
  <nav id="menu" style="background: #2f2;font-size: 12px;">
    <ul>
      <li data-foo="bar" data-set="ok">one</li>
      <li class="sec">two</li>
      <li download>three</li>
    </ul>
  </nav>
  <h2 id="title" style="background-color: red;">content title</h2>
  <p class="first foobie">so it's just like a templating engine,
    but easy to use inline with javascript
  </p>
  <p>the intention is for this to be used to create
  <strong style="background: white; color: green">charlike</strong>
  reusable, interactive html widgets. 
  </p>
</div>
tunnckoCore commented 7 years ago

Stateless Components

var Foo = function ({ name, last, children }) {
  return h('user', { name, last }, children)
}
var foo = h(Foo, {
  name: 'Charlike',
  last: 'Reagent'
}, 'Hello')

console.log(foo)

with toHtml it yields

<user name="Charlike" last="Reagent">Hello</user>
tunnckoCore commented 7 years ago

Chika is girlfriend Mich, the Virtual Sword guy.

They kinda have childrens - Gibon and Prince Mich Rich Router

tunnckoCore commented 7 years ago

kinda interesting signatures and stuff. For mich and chika.

  1. The whole thing is that there should be one export, that gets single model argument - reducers, effects, subscriptions, state, hooks. Say app(model)
  2. Then it returns a function, call it CreateView - it accepts signle argument - a function
  3. That function passed to CreateView is passed with { state, actions, props, children, params } - some of them may not exist - depends on use
  4. that createView returns a function - actual component, say TodoItemComponent
  5. it can be invoked as usual - TodoItemComponent({ props: { foo: 'bar' }, state: { hi: 'wi' } })
  6. or using JSX - <TodoItemComponent foo="bar" state={{ hoho: 'hehe' }} /> - allowing to merge/change the initial state that was passed to the 1), while creating the model.
  7. There should be non-components approach with just model and view
  8. For that just use opts.view or directly view fn to the main export app(({ state, actions, props }) => <h1 />)
/** @jsx h */
const h = require('mich-h')

// const List = (model) => ({ props, children}) =>

// const CatList = app({
//   subscriptions: {},
//   reducers: {},
//   effects: {},
//   hooks: {}
//   state: {}
// })

// const tree = <CatList name="charlike" />
// const tree = CatList((state, actions) => <div />)

// render(tree, document.body)

function app (options) {
  let state = options.state

  const actions = {}
  const subscriptions = options.subscriptions || {}
  const reducers = options.reducers || {}
  const effects = options.effects || {}
  const hooks = merge({
    onAction: Function.prototype,
    onUpdate: Function.prototype,
    onError: (er) => { throw er }
  }, options.hooks)

  return function Component (ctx) {
    if (typeof ctx === 'function') {
      let prev = null
      let viewFn = ctx

      // using `var` intentionally
      for (var name in merge(reducers, effects)) {
        actions[name] = function action (data) {
          hooks.onAction(name, data)

          // double `effects[name]` for saving bytes
          if (effects[name]) {
            return effects[name](state, actions, data, hooks.onError)
          }

          const oldState = state
          state = reducers[name](oldState, data)

          prev = viewFn(state, actions)

          hooks.onUpdate(oldState, state, data)
        }
      }
      return viewFn(state, actions)
    }
  }
}

// const TodoItemComponent = Component({
//   reducers: {},
//   effects: {},
//   state: {
//     hi: 'hello',
//     link: '#initial'
//   }
// })

// const TodoItem = TodoItemComponent(({ state, props, actions }) => (<li>
//   <a href={state.link} onclick={actions.handleClick}>some {state.hi}</a>
// </li>))

// render(<TodoItem />, document.body)
// render(TodoItem(), document.body)

const View = Component({
  state: { welcome: 'Hello World', aboutme: '#hoho-about-me' }
})

// const CreateView = CreateAppModel({})
// const AnchorComponent = CreateView()
//
// // variant 1
// let tree = AnchorComponent({
//   props: { foo:bar }
// })
// render(tree, document.body)
//
// // variant 2
// let ret = <AnchorComponent />
// let ret = <AnchorComponent foo="bar" state={{ hi: 'welcome' }} />
// render(ret, document.body)

// same component state, different view
const Link = View(({ state, props, children }) => <a href={props.path} title={state.welcome}>
  {children}
</a>)

const Main = View(({ state, actions, props }) => <div>
  <h1>{state.welcome}</h1>
  <h2>Is there any props? {props}</h2>
  <Link path="/about">{state.aboutme}</Link>
</div>)

render(<Main />, document.body)
// render(main(), document.body)

// const ls = <TodoItem />
// render(<TodoItem state={{
//   hi: 'non def',
//   link: '#new-link'
// }} />, document.body)

// using `var` intentionally
// for (var name in merge(reducers, effects)) {
//   actions[name] = function action (data) {
//     hooks.onAction(name, data)

//     // double `effects[name]` for saving bytes
//     if (effects[name]) {
//       return effects[name](state, actions, data, hooks.onError)
//     }

//     const oldState = state
//     state = reducers[name](oldState, data)

//     tree = render(tree, state, actions)

//     hooks.onUpdate(oldState, model, data)
//   }
// }

// function render (node, root) {
//   if (!node) return
//   root.appendChild(createElement(node))
// }

// function createElement (node) {
//   if (node && node.type === 'text') {
//     return document.createTextNode(node.value)
//   }
//   if (node && node.type === 'element') {
//     var $el = document.createElement(node.tagName)
//     for (var i in node.children) {
//       var el = createElement(node.children[i])
//       $el.appendChild(el)
//     }
//     return $el
//   }
// }

// function updateElement($parent, newNode, oldNode) {
//   if (!oldNode) {
//     $parent.appendChild(
//       createElement(newNode)
//     );
//   }
// }

// const Item = (props) => (<li>
//   {props.name}
// </li>)

// const List = (props) => (
//   <ul className="list">
//     {props.items.map((item) => <Item name={item}/>)}
//   </ul>
// )

// const $root = document.body
// const items = [
//   'hello world',
//   'foo bar',
//   'hehehe'
// ]

// render(<List items={items}/>, $root)

// const data = {
//   age: 24,
//   firstName: 'Charlike',
//   description: 'The `mich-h` is pretty good virtual dom builder'
// }

// const Form = (props) => <form className="form">
//   <div className="form-group">
//     <label>Please enter your first name:</label>
//     <input type="text" placeholder="Joe" value={props.name}/>
//   </div>
//   <div className="form-group">
//     <label>Please enter your last name:</label>
//     <input type="text" placeholder="Bloggs" value="Gannaway"/>
//   </div>
//   <div className="form-group">
//     <label>Please enter your age:</label>
//     <input type="number" min="0" max="99" value={props.age}/>
//   </div>
//   <div className="form-group">
//     <label>What is your favourite food:</label>
//     <div className="inline"><input type="radio" name="food"/><span>Pizza</span></div>
//     <div className="inline"><input type="radio" name="food"/><span>Pasta</span></div>
//   </div>
//   <div className="form-group">
//     <label>Please enter your location:</label>
//     <select>
//       <option>United States</option>
//       <option>United Kingdom</option>
//       <option>France</option>
//     </select>
//   </div>
//   <div className="form-group">
//     <label>Please enter a description:</label>
//     <textarea value={props.desc}>I dont know?</textarea>
//   </div>
// </form>

// render(<Form
//   age={data.age}
//   name={data.firstName}
//   desc={data.description}
// />, document.body)
tunnckoCore commented 7 years ago

Components, continued.

  1. We'll have an app() that gets a model - reducers, effects, hooks, initial state
  2. That app() returns a function CreateView a view function
  3. That CreateView({ state, actions, props, params, children }) returns a function, Component
  4. That component is passed with props, children signature. That function usually would be called when you use JSX syntax like <Router foo="bar"></Router> so `Route({ props: { foo: 'bar' } }.
  5. It returns a virtual dom tree

Everything is just fine tuned to work well for usual views approach, and for components approach too.

function app (options) {
  options = options || {}
  let state = options.state

  const actions = {}
  const subscriptions = options.subscriptions || {}
  const reducers = options.reducers || {}
  const effects = options.effects || {}
  const hooks = merge({
    onAction: Function.prototype,
    onUpdate: Function.prototype,
    onError: (er) => { throw er }
  }, options.hooks)

  return function createView (viewFn) {
    return function Component (props, children) {
      if (Object.keys(options).length === 0) return viewFn({ props, children })

      for (var name in merge(reducers, effects)) {
        actions[name] = function action (data) {
          hooks.onAction(name, data)

          // double `effects[name]` for saving bytes
          if (effects[name]) {
            return effects[name]({ state, data, actions }, hooks.onError)
          }

          const oldState = state
          state = reducers[name]({ state, data })

          viewFn({ state, actions, props, children })

          hooks.onUpdate(oldState, state, data)
        }
      }

      return viewFn({ state, actions, props, children })
    }
  }
}

Examples

const CatListView = app({
  subscriptions: {},
  reducers: {
    add: ({ state, data }) => ({ foo: data.foo })
  },
  effects: {
    get: ({ state, data, actions }, done) => fetch('foobar.com', done)
  },
  hooks: {
    onUpdate: (oldState, state, data) => {}
  }
  state: { some: 'initial-state' }
})

const CatList = CatListView(({ state, actions, props, params, children }) => (<p>
  <a href="/cats/{state.some}">{props.name}</a>
</p>))

const tree = <CatList name="charlike"/>
// or just
// const tree = CatList({ state: { foo: 123 }, props: { name: 'charlike' } })
render(tree, document.body)

Kinda router?

var routeMatch = require('path-match')()

const Router = app()(({ children }) => children.map((route) => {
  var match = routeMatch(route.properties.path)
  var params = match(window.location.pathname)

  if (route.children.length) {
    return params ? route.children : null
  }

  return params ? route.properties.component({ params }) : null
}))

const tree = <Router>
  <route path="/" component={CatList} />
  <route path="/about">
    <About name="charlike" />
  </route>
  <route path="/users/:user" component={User} />
</Router>
tunnckoCore commented 7 years ago

with current mich

const Router = ({ path, component, children }) => {
  console.log('actual')
  // component is function CatList
  return component({ name: path, children })
}
const CatList = ({ name, children }) => <p>{name}/foobie</p>
const tree = <Router path="/cats" component={CatList} />
console.log(tree)
tunnckoCore commented 7 years ago

Loading Component

import loading from './raptor-loading.gif'

const Loading = app()(({ props, children }) => (<div className="Loading">
  {
    !props.error ? (
      <img className="Loading-img" src={loading} alt="Loading..." />
    ) : (
      <p className="alert alert-danger"><strong>Error:</strong> Could not retrieve data.</p>
    )
  }
</div>))

// const tree = <Loading />
export default Loading
tunnckoCore commented 7 years ago

Exporting components

/** @jsx h */
import { Component, h } from 'chika'

// var SomeClassComponent = {
//   render(props) {
//     return (<div>Hello {props.place || 'World'}!</div>)
//   }
// }

// think for this as above
// Component(viewFn) => returns a component
export default Component(({ props }) => <div>Hello {props.place || 'World'}!</div>)

later where component is needed

/** @jsx h */
import { h, render, renderToString } from 'chika'
import HelloWorld from './HelloWorld'

render(<HelloWorld />, document.body)
render(<HelloWorld place="Charlike" />, document.body)

// server-side rendering
renderToString(<HelloWorld place="folks" />, someState)
tunnckoCore commented 7 years ago

chika for nodejs

import h from 'mich-h'
import app from './app'
import renderToString from 'mich-to-html'

const Component = (viewFn) => app()(viewFn)

export default {
  h, // mich-h
  app, // 37 lines of cool code
  renderToString, // mich-to-html
  Component // thin wrapper over `app`
}

chika for browser

import h from 'mich-h' // ~500 bytes
import app from './app' // ~300 bytes
import render from 'mich-to-dom' // ~500 bytes

const Component = (viewFn) => app()(viewFn)

export default {
  h,
  app,
  render,
  Component
}
tunnckoCore commented 7 years ago

Just for reference http://dfilatov.github.io/vidom/playground/, how similar is our approach.

tunnckoCore commented 7 years ago

a link to a svg, maybe logo for mich

data:image/svg+xml;base64,PHN2ZyBmaWxsPSIjRUM1RDU3IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjEiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMTAwIDEwMCIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMTAwIDEwMCIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PGc+PHBvbHlnb24gcG9pbnRzPSIzNy44OTEsNDcuMDg2IDM1LjczOSw1MC44MTQgMzcuODkxLDU0LjU0MiA0Mi4xOTYsNTQuNTQyIDQ0LjM0OSw1MC44MTQgNDIuMTk2LDQ3LjA4NiAgIi8+PHBvbHlnb24gcG9pbnRzPSI1Ny44MDUsNDcuMDg2IDU1LjY1MSw1MC44MTQgNTcuODA1LDU0LjU0MiA2Mi4xMDksNTQuNTQyIDY0LjI2Miw1MC44MTQgNjIuMTA5LDQ3LjA4NiAgIi8+PHBhdGggZD0iTTg3Ljg0NiwzNC45NTVsLTEyLjI3OS0xLjE2N2wtMS45NjIsNC4yNjVsLTcuMjYtMTIuNTczaC03LjMxM0w0OC42NjgsMTUuMzMzbC0wLjc5LDQuMzkybC00LjI0NS03Ljk5NGwtMC44MzgsNS41MjcgICBsLTQuMzQzLTcuOTQ1bC0wLjUzMSw1LjMxN0wzMi41Myw2LjEwNGwzLjQ1NiwxOS4zNzVoLTIuMzMxbC03LjI1OSwxMi41NzNsLTEuOTYyLTQuMjY1bC0xMi4yOCwxLjE2N0w1LjAzLDQ3LjMyNmw1LjE1NSwxMS4yMDYgICBsOS4zNDktMC44ODlsOS41MDIsMTYuNDU5bC0wLjY2NywxLjA2OGwtMC43ODQsMS4yNTZsMC43ODQsMS4yNTVMMzcuOCw5Mi43ODJsMC42OTUsMS4xMTNoMS4zMTNoMjAuMzgzaDEuMzEzbDAuNjk2LTEuMTEzICAgbDkuNDMxLTE1LjEwMWwwLjc4NC0xLjI1NWwtMC43ODQtMS4yNTZsLTAuNjY4LTEuMDY4bDkuNTAzLTE2LjQ1OWw5LjM0OSwwLjg4OWw1LjE1NS0xMS4yMDZMODcuODQ2LDM0Ljk1NXogTTIwLjA3NSw1Mi4xNyAgIGwtNi41ODcsMC42MjdsLTIuNzY1LTYuMDEybDMuODIxLTYuNjM2bDYuNTg3LTAuNjI2bDIuNzY1LDYuMDExTDIwLjA3NSw1Mi4xN3ogTTU4LjQyMiw4OC45MDZINDEuNTc3bC03Ljc5NC0xMi40NzlsNy43OTQtMTIuNDgxICAgaDE2Ljg0NWw3Ljc5NSwxMi40ODFMNTguNDIyLDg4LjkwNnogTTY3LjAyNyw2Ny44MDFMNjIuMiw2MC4wNzFsLTAuNjk2LTEuMTE1aC0xLjMxM0gzOS44MDloLTEuMzEzTDM3LjgsNjAuMDcxbC00LjgyOSw3LjcyOSAgIGwtNS42NjctMTUuMjZMMzcuNjYzLDM1Ljk2aDkuMzMxTDUwLDQyLjk2N2wzLjAwNy03LjAwN2g5LjgyNGw5Ljg2NSwxNi41ODFMNjcuMDI3LDY3LjgwMXogTTg2LjUxMiw1Mi43OTdsLTYuNTg3LTAuNjI3ICAgbC0zLjgyMS02LjYzNmwyLjc2Ni02LjAxMWw2LjU4NywwLjYyNmwzLjgyLDYuNjM2TDg2LjUxMiw1Mi43OTd6Ii8+PC9nPjxwb2x5Z29uIHBvaW50cz0iNjIuMzQyLDc3LjIzNSA2MS4yNzEsNzUuMTIzIDU1Ljk3NCw3Ny44MDggNDQuMjcxLDc3LjgwOCAzOS4zNTEsNzUuMTM5IDM4LjIyMSw3Ny4yMjEgNDMuOTcsODAuMTc3IDU2LjI1Nyw4MC4xNzcgICAiLz48L3N2Zz4=
tunnckoCore commented 7 years ago

:tada: AWESOME PROGRESS ON CHIKA / MICH !! :taco:

/** @jsx h */
// const { h, app, el, renderToString } = require('./chika')

/**
 * API
 */

// mich-component
// export default function Component (view, state, actions) {
//   return (props, children) => view({ state, actions, props, children })
// }

// mich-mount
// import render from 'mich-render'
// export default mount
function mount (ast, dom) {
  var el = ast.type && ast.tagName && ast.properties && ast.children
    ? render(ast)
    : ast
  dom.appendChild(el)
  return dom
}

// mich-render
// export default render
function render (node) {
  if (node && node.type === 'text') {
    return document.createTextNode(node.value)
  }
  if (node && node.type === 'element') {
    var element = document.createElement(node.tagName)
    setAttributes(element, node.properties)

    for (var i in node.children) {
      var el = render(node.children[i])
      el && element.appendChild(el)
    }

    return element
  }
}

function setAttributes (el, name, value) {
  if (arguments.length > 2) {
    if (name === 'style') {
      setStyle(el, value)
    } else if (name.slice(0, 2) === 'on') {
      el.addEventListener(name.slice(2).toLowerCase(), value)
    } else if (typeof value === 'boolean') {
      setBooleanProp(el, name, value)
    } else {
      var transform = {
        htmlFor: 'for',
        className: 'class',
        httpEquiv: 'http-equiv'
      }
      name = transform[name] ? transform[name] : name
      el.setAttribute(name, value)
    }
  } else {
    for (var key in name) {
      setAttributes(el, key, name[key])
    }
  }
}

function setStyle (el, name, value) {
  if (arguments.length > 2) {
    el.style = el.style || {}
    el.style[name] = value
  } else if (typeof name === 'string') {
    el.setAttribute('style', name)
  } else {
    for (var key in name) {
      setStyle(el, key, name[key])
    }
  }
}

function setBooleanProp (el, name, value) {
  if (value) {
    el.setAttribute(name, value)
    el[name] = true
  } else {
    el[name] = false
  }
}

// mich
// import Component from 'mich-component'
// import render from 'mich-render'
// import mount from 'mich-mount'
// import h from 'mich-h'

// export default {
//   Component,
//   render,
//   mount,
//   app: mich,
//   h
// }

/** @jsx h */
// import { Component, mount, h } from 'mich'
// const HelloCharlike = Component(({ props }) => `<h1 {...props}>Hello Charlike</h1>`)

// mount(<HelloCharlike />, document.body)

/**
 * Chika
 */

/**
 * Example
 */

var h = require('mich-h')

function merge (foo, bar, key) {
  for (key in bar) {
    foo[key] = bar[key]
  }
  return foo
}

function michApp (view, model) {
  if (!model) {
    return michComponent(view)
  }

  var actions = {}
  var subscriptions = model.subscriptions || {}
  var reducers = model.reducers || {}
  var effects = model.effects || {}
  var state = model.state || {}
  var root = model.root
  var hooks = model.hooks = merge({
    onStart: (context, model) => {},
    onInit: (context, model) => {},
    onWillUpdate: (context, model, oldEl) => {},
    onDidUpdate: (context, model, oldEl) => {},
    onWillMount: (context, model) => {},
    onDidMount: (context, model) => {},
    onAction: (name, context) => {},
    onError: (er) => { throw er },
    onRender: (context, model, view, el) => view(context)
  }, model.hooks)

  var oldEl = null

  // actions start
  function createAction (name, props, children) {
    return function _action (data) {
      var context = {
        state,
        data,
        props,
        children
      }

      hooks.onAction(name, context, model)

      // double `effects[name]` for saving bytes
      if (effects[name]) {
        context.actions = actions
        return effects[name](context, hooks.onError)
      }

      // context.state is the old state
      hooks.onWillUpdate(context, model)

      const oldState = merge({}, state)
      const newState = reducers[name](context)

      context.state = state = merge(oldState, newState)
      context.actions = actions

      oldEl = hooks.onRender(context, model, view, oldEl)

      // context.state is the new state,
      // so that's why we pass old state as 3rd arg
      hooks.onDidUpdate(context, model, oldState)
    }
  }
  // actions end

  hooks.onStart({ state }, model)
  return (props, children) => {
    // !! NOTICE: actions should not be created here?!?!
    // Because if they are here it will call the for loop,
    // merge effects and reducers for each `<HelloWorld />` call
    // which isn't pretty okey, because the component could be called
    // more than once.
    // So it will be too consuming if actions creation is here.
    // But if it isn't here, the view called from action can't
    // access the `props, children`, sooo.. WAT TO DO?

    for (var name in merge(reducers, effects)) {
      actions[name] = createAction(name, props, children)
    }

    var context = { state, actions, props, children }
    hooks.onInit(context, model)

    return (oldEl = hooks.onRender(context, model, view, root))
  }
}

// the "context"
// - in all places `context` is 1st argument, except in `onAction` where is 2nd
// - if in onStart: { state }
// - if in effect:  { state, data, props, children, actions }
// - if in reducer: { state, data, props, children }
//
// 1. onStart({ start }, model)
// 2. onInit({ state, actions, props, children }, model)
//
// if it is initial render, won't have `data`
// this applies for `onRender`, `onWillMount` and `onDidMount`
//
// 3. onRender({ state, actions, props, children[, data] }, model, viewFn, oldEl)
// 4. onWillMount({ state, actions, props, children[, data] }, model)
//
// 5. onAction(name, { state, actions, props, children, data }, model)
// 6. onWillUpdate({ state, props, children, data }, model)
//  -- actual recall of view and again onRender
//
// 7. onDidUpdate({ state, actions, props, children, data }, model, oldState)
// 8. onDidMount({ state, actions, props, children[, data] }, model)

/**
 * !!! CHIKA !!!
 */

var update = require('nanomorph/update')

function chika (view, model) {
  if (!model) {
    return michComponent(view)
  }

  var morph = null
  model = merge({}, model)

  model.hooks = merge({
    onInit: (context, _model) => {
      document.addEventListener('DOMContentLoaded', () => {
        for (var sub in _model.subscriptions) {
          _model.subscriptions[sub](context, _model.hooks.onError)
        }
      })
      morph = update(_model.root)
    },
    onRender: (context, _model, view, oldNode) => {
      _model.hooks.onWillMount(context, _model)
      var el = morph(render(view(context)), oldNode)
      _model.hooks.onDidMount(context, _model)
      return el
    }
  }, model.hooks)

  return michApp(view, model)
}

// export default {
//   Component: chika,
//   render: mich.render,
//   mount: mich.mount,
//   h: mich.h
// }

/**
 * !!! END CHIKA !!!
 */

const model = {
  root: document.body,
  state: {
    place: 'World',
    time: new Date()
  },
  reducers: {
    welcome: ({ state, data }) => ({ place: data.text }),
    time: ({ state, data }) => ({ time: data.date })
  },
  subscriptions: [
    (context, done) => {
      setInterval(() => {
        context.actions.time({ date: new Date() })
      }, 1000)
    },
    (context, done) => {
      setTimeout(() => {
        context.actions.welcome({ text: 'sweety cat'})
      }, 15000)
    }
  ]
}

const HelloWorld = chika(({ state, actions, props }) => {
  return h('.main', [
    h('h1', {
      onclick: (e) => actions.welcome({ text: 'Charlike' })
    }, `Hello ${state.place}!`),
    h('p', `The time is ${state.time}`)
  ])
}, model)

const el = HelloWorld({ id: 'hero' })
// mount(el, document.body)
// console.log(render(el))

// * @jsx h
// import { Component, h } from 'chika'
// export default Component(({ state, actions, props, children }) => <h1>Hello!</h1>)
// mount(<HelloWorld />, document.body)

// function Component (view, state, actions) {
//   return function CreateComponent (props, children) {
//     return view({ state, actions, props, children })
//   }
// }

// // Stateful Component
// const state = { place: 'World' }
// const view = ({ state, props, actions }) => h('h1', props, `Hello ${state.place}`)
// const HelloWorld = Component(view, state, actions)

// // Stateless Component
// const HelloCharlike = Component(({ props }) => h('h1', props, 'Hello Charlike'))

// var hi = render(HelloWorld({ id: 'hero' }))
// // mount(hi, document.body)

// mount(HelloCharlike({ className: 'yea' }), document.body)

// const TodoListView = app({
//   state: {
//     todos: [{
//       id: 1,
//       done: false,
//       text: 'initial todo'
//     }],
//     count: 0,
//     input: '',
//     placeholder: 'Add new todo'
//   },
//   reducers: {
//     toggle: ({ state, data }) => ({
//       todos: state.todos.map((item) => {
//         return data.id === item.id
//           ? Object.assign({}, item, { done: !data.done })
//           : item
//       })
//     }),
//     remove: ({ state, data }) => ({
//       todos: state.todos.map((todo) => todo.id !== data.id)
//     }),
//     add: ({ state, data }) => ({
//       todos: state.todos.concat({
//         id: state.count++,
//         done: false,
//         text: state.input
//       })
//     }),
//     input: ({ state, data }) => ({ input: data.input }),
//   }
// })

// const TodoList = TodoListView(({ state, actions }) => <section>
//   <ul>
//     { state.todos.map((item) => (<li onclick={e => actions.toggle(item)}>
//       {item.text}
//     </li>)) }
//   </ul>
//   <p>
//     <input
//       className="add todo--item"
//       type="text"
//       onkeyup={e => e.keyCode === 13 ? actions.add() : ''}
//       oninput={e => actions.input({ input: e.target.value })}
//       placeholder={state.placeholder}
//     />
//     <div onclick={actions.add}>add</div>
//   </p>
// </section>)

// const MainView = app({
//   state: {
//     title: 'Todo App',
//     link: 'https://github.com/tunnckoCore/chika'
//   }
// })
// const Main = MainView(({ state }) => <main id="root">
//   <h1>{state.title}</h1>

//   <footer>Powered by <a href={state.link}>Chika</a></footer>
// </main>)

// document.body.appendChild(el(<Main />))
tunnckoCore commented 7 years ago

monorepo

tunnckoCore commented 7 years ago

Just state management plugin for dush so can create apps easy

var dush = require('dush')

function faz (model) {
  const { state, reducers, effects } = model

  return dush().use((app) => {
    app.state = Object.assign({}, state)
    const acts = Object.assign({}, reducers, effects)

    app.actions = Object.keys(acts).reduce((actions, name) => {
      actions[name] = (data) => {
        app.emit('action', name, data)

        const oldState = Object.assign({}, app.state)

        if (effects[name]) {
          const done = (er) => {
            if (er) app.emit('error', er)
          }

          app.emit('effect', name, oldState, actions, data)
          return effects[name](oldState, actions, data, done)
        }

        app.emit('reducer', name, oldState, data)
        let newState = Object.assign({}, oldState, reducers[name](oldState, data))

        app.state = newState
        app.emit('update', newState, oldState, data)
      }

      return actions
    }, {})
    app.actions.emit = app.emit
  })
  .use((app) => {
    app.Component = (view) => (props) => view(props, app.actions, app.state)
  })
}

example


const app = faz({
  state: {
    text: 'Hello',
    place: 'World'
  },
  effects: {
    foo: (state, actions, data, done) => {},
    bar: (state, actions, data) => {
      actions.set({ zaz: true, txt: data.text })
    }
  },
  reducers: {
    qux: (state, data) => {},
    redirect: (state, data) => (Object.assign(data, { set: true }))
  }
})

const html = require('bel')

app.on('quxie', (e) => {
  app.state = Object.assign({}, app.state, data)
})

//                                  props      actions
const HelloWorld = app.Component(({ place }, { redirect, emit }, state) => html`<div>
  <h1 onclick=${redirect}>${state.text} ${place}</h1>
  <input type="text" oninput=${(e) => emit('quxie', { text: e.target.value }} />
</div>`)

console.log(HelloWorld({ place: 'World' }).toString())
tunnckoCore commented 7 years ago

Powerful and flexible actions plugin for dush

"Small & powerful state management a la Elm, based on event system."

emitting error (use .once), stateUpdate and action events


'use strict'

module.exports = function actionsPlugin (state, actions) {
  return function actionsPlugis (app) {
    app.actions = {}
    state = Object.assign({}, state)

    Object.keys(actions).forEach((name) => {
      const action = createAction(app, name)

      // allow calling actions explicitly, instead of "emitting" them; e.g.
      // - app.actions.fooBar({ some: 'data' })
      app.actions[name] = action

      // and the "emitting" style
      // - app.emit('fooBar', { some: 'data' })
      app.on(name, action)
    })

    return app

    function createAction (app, name) {
      return function action (data) {
        app.emit('action', name, data)

        // merge `actions` into `emit` function
        // so these two style are allowed:
        // - (state, actions, data) -> actions.fooBar({ some: 'data' })
        // - (state, emit, data) -> emit('fooBar', { some: 'data' })
        const emit = Object.assign(app.emit, app.actions)

        // call the action
        const ret = actions[name](state, emit, data, done)

        // allow asynchronous actions that
        // returns new state, or call another action(s)
        // - async (state, actions, data) => delay(2200).then(() => ({
        //   foo: data.foo
        // }))
        // - async (state, actions, data) => delay(2200).then(() => {
        //   actions.fooBar({ foo: data.foo })
        // })
        if (isPromise(ret)) {
          ret.then(updateState).catch(done)
          return
        }

        // allow synchronous actions, if they return
        // non-falsey value it is merged with the state
        updateState(ret)
      }
    }

    function done (er) {
      if (er) app.emit('error', er)
    }

    function updateState (data) {
      if (!data) {
        return state
      }

      const oldState = state
      state = Object.assign({}, state, data)
      app.emit('stateUpdate', state, oldState, data)
    }
  }
}

function isPromise (val) {
  return val instanceof Promise || (
    val !== null &&
    typeof val === 'object' &&
    typeof val.then === 'function' &&
    typeof val.catch === 'function'
  )
}

example

const app = dush().use(dushActions(state, actions))

app.on('stateUpdate', (state, prev, data) => {
  console.log('state:', state)
  console.log('prev:', prev)
})

app.emit('changeName', { foo: 'Base Emit' })
app.emit('changeName', { foo: 'Foobie Emit' })
// or
// app.actions.changeName({ foo: 'Base Fn' })
// app.actions.changeName({ foo: 'Foobie' })

with these state, actions

const state = {
  text: 'Hello',
  name: 'Charlike'
}

const actions = {
  // effect, call `emit` argument "emit" or "actions"
  changeName: (state, emit /* or actions */, data) => {
    // style 1 - async action (effect) that directly update the state 
    // return delay(2220).then(() => ({ name: data.foo }))

    // style 2
    // return delay(2220).then(() => {
    //   actions.setName({ foo: data.foo })
    // })

    // style 3
    return delay(1220).then(() => {
      emit('setName', { foo: data.foo })
    })

    // style 4
    // setTimeout(() => {
    //   emit('setName', { foo: data.foo })
    // }, 2220)

    // style 5
    // setTimeout(() => {
    //   actions.setName({ foo: data.foo })
    // }, 1500)
  },

  // reducer
  setName: (state, _, data) => {
    // style 6
    return { name: data.foo }
  }
}
tunnckoCore commented 7 years ago

sample app, probably will wrok soon


var dush = require('dush')
var delay = require('delay')
var dushRouter = require('dush-router')
var dushActions = require('./dush-actions-plugin')
var dushBrowser = require('./dush-router-browser-plugin')

/**
 * EXAMPLE
 */

const state = {
  text: 'Hello',
  name: 'Charlike'
}

const actions = {
  // effect
  changeName: (state, emit, data) => {
    // style 1
    // return delay(2220).then(() => ({ name: data.foo }))

    // style 2
    // return delay(2220).then(() => {
    //   actions.setName({ foo: data.foo })
    // })

    // style 3
    return delay(1220).then(() => {
      emit('setName', { foo: data.foo })
    })

    // style 4
    // setTimeout(() => {
    //   emit('setName', { foo: data.foo })
    // }, 2220)

    // style 5
    // setTimeout(() => {
    //   actions.setName({ foo: data.foo })
    // }, 1500)
  },

  // reducer
  setName: (state, emit, data) => {
    // style 6
    return { name: data.foo }
  }
}

/**
 * APP
 */
require('undom/register')
const html = require('bel')
const nanomorph = require('nanomorph')

function chika (state, actions) {
  const app = dush()
    .use(dushRouter())
    // .use(dushBrowser())
    .use(dushActions(state, actions))

  app.on('stateUpdate', (_state) => {
    state = _state
  })

  let tree = document.createElement('div')
  let data = {}

  app.off('route')
  app.on('route', function (view, context) {
    const match = {
      pathname: context.pathname,
      params: context.params,
      route: context.route
    }
    return (tree = nanomorph(tree, view(context.state, app.emit, match)))
  })

  app.start = function () {
    tree = app.navigate(window.location.pathname, state)

    app.on('render', function (node, hist) {
      if (hist) {
        window.history.replaceState(data, '', node.pathname)
      } else {
        window.history.pushState(data, '', node.pathname)
      }
      app.navigate(node.pathname, state)
    })

    app.on('historyChange', function (node) {
      app.emit('render', node, true)
    })
    app.on('hrefChange', function (node) {
      if (node.href === window.location.href) return

      app.emit('render', node)
    })

    return tree
  }

  app.mount = function mount_ (selector) {
    var newTree = app.start()
    var main = document.querySelector(selector)
    tree = nanomorph(main, newTree)
  }

  return app
}

/**
 * USAGE CHIKA
 */

const app = chika(state, actions)

app.addRoute('/users/:id', (state, actions, match /* === { params, pathname, route }*/) => {
  return html`<h1>${state.text}</h1>`
})

// console.log(app.navigate('/users/charlike'))
tunnckoCore commented 7 years ago

one awesome guide with cool ideas and signatures: https://github.com/FormidableLabs/freactal

effects / actions are just:

tunnckoCore commented 7 years ago

Just don't want to open new issue or push some commits. I'll just delete that comment after some time. So temporary:

Двигaтeл нa либepaлнaтa иĸoнoмиĸa cтaнa глoбaлизaциятa. A тpитe cтълбa, въpxy ĸoитo тя ce ĸpeпи ca cвoбoднaтa тъpгoвия, cвoбoднoтo движeниe нa ĸaпитaли и cвoбoднoтo движeниe нa paбoтнa pъĸa oт бeднитe ĸъм бoгaтитe cтpaни. Днec oбaчe cвoбoднaтa тъpгoвия зaбaви cвoитe oбopoти, дaжe мoжe дa ce ĸaжe, чe cъвceм зaмpя пo peд пoлитичecĸи и дpyги пpичини. Πpoцecът нa cвoбoднo движeниe нa ĸaпитaли тъpпи вce пoвeчe oгpaничeния, cвъpзaни c пpoблeмитe нa финaнcoвaтa cигypнocт и битĸaтa c тepopизмa. Дaжe пoвeчe ce гoвopи зa „бягcтвo” нa ĸaпитaлa ĸъм oфшopнитe зoни, oтĸoлĸoтo зa cвoбoднoтo движeниe нa ĸaпитaлoвитe пoтoци в глoбaлнaтa финaнcoвa мpeжa. Cвoбoднoтo движeниe нa xopa cлeд нaxлyлитe в Eвpoпa вълни oт бeжaнци нaĸapa няĸoи oт eвpoпeйcĸитe дъpжaви дa пocтaвят физичecĸи бapиepи нa дъpжaвнитe cи гpaници, зa дa cпpaт бeжaнcĸия пoтoĸ. C тoвa дe фaĸтo ce oбeзcмиcли и пocлeднoтo пpeдимcтвo, пpипиcвaнo нa глoбaлизaциятa. Ho дopи и бeз няĸaĸъв пo-cepиoзeн aнaлиз нa peзyлтaтитe oт пpилaгaнeтo нa либepaлния мoдeл, a caмo нa бaзaтa нa иcтopичecĸия oпит oт пpeзидeнтa Peйгън нacaм, e виднo, чe тaзи иĸoнoмичecĸa cиcтeмa мoжe дa фyнĸциoниpa eдинcтвeнo нa пpинципитe нa измaмaтa и гpaбeжa. Caмa пo ceбe cи cиcтeмaтa нe мoжe дa ce възпpoизвeждa бeз пocтoяннo, външнo финaнcиpaнe. Oтнaчaлo тя пapaзитиpa въpxy гъpбa нa paзпaднaлия ce CCCP. Πo-ĸъcнo пpoблeмитe пpoличaвaт cлeд измaмaтa c „oтpoвни” дepивaти и гpaбeжa нa бaнĸитe пpeз 2008 г. Toгaвa CAЩ, зa дa пpeдoтвpaтят тoтaлния ĸoлaпc нa бaнĸитe пpиexa пoлитиĸa нa т.нap. „ĸoличecтвeни вливaния”, c цeл дa cтимyлиpaт инвecтициитe в индycтpиятa и дa извeдaт иĸoнoмиĸaтa oт дълбoĸaтa peцecия, в ĸoятo e изпaднaлa . Ceгaшният пpoдължитeлeн пepиoд бeз вoйни, пpeз ĸoйтo paзвититe дъpжaви пpaĸтичecĸи нямa oт ĸoгo дa изcмyĸвaт pecypcи, дoвeдe дo ĸpизa зa тeзи oт тяx, ĸoитo ce нaмиpaт нa въpxa нa „xpaнитeлнaтa вepигa”. Либepaлнaтa иĸoнoмичecĸa cиcтeмa бeз „пpeливaнe нa пapи” ĸoлaбиpa . Heoбxoдимocттa oт Tpeтa cвeтoвнa вoйнa (или възмoжнocттa зa чepпeнe нa cвeж pecypc oт пoбeдeнитe cтpaни) e жизнeнo нeoбxoдимa зa oцeлявaнeтo нa пoвeчeтo зaпaдни дъpжaви и пo-cпeциaлнo зa пpиĸaзнo бoгaтитe им eлити. Ho oпacнocттa зa Зaпaдa идвa oт тoвa, чe зaпoчвaнeтo нa eднa нoвa вoйнa нe caмo чe нямa дa peши, a oщe пoвeчe щe зaдълбoчи пpoблeмитe им. Днec нямa тoлĸoвa cлaби вpaгoвe, ĸoитo c лeĸoтa дa бъдaт пoбeдeни и дa cтaнaт дoнopи нa зaĸъcaлитe зaпaдни дъpжaви . Ceгa пpиблизитeлният пapитeт нa cилитe в cвeтa e тaĸъв, ĸaĸъвтo бeшe и пpeди двeтe cвeтoвни вoйни, ĸoeтo caмo пo ceбe cи пoвишaвa pиcĸa oт нoв cвeтoвeн ĸoнфлиĸт. Toй мoжe дa бъдe oт ĸлacичecĸи тип, ĸaĸъвтo бeшe в пpeдишнитe двe вoйни, нo мoжe дa бъдe и xибpидeн, зaмacĸиpaн пoд гoлямo ĸoличecтвo oт лoĸaлни ĸoнфлиĸти. Глaвнaтa цeл нa opгaнизaтopитe нa тeзи мнoгoбpoйни ĸoнфлиĸти e, нapeд c иĸoнoмичecĸaтa и инфopмaциoннa aгpecия, ĸoятo вoдят, дa ycпeят дa пapиpaт възмoжнocттa нa пpoтивниĸa дa yпoтpeби ядpeнo opъжиe . Бeлeзитe нa тaзи cтpaтeгия ca cъвceм oчeвидни. Ha пъpвo мяcтo, тoвa e пpeĸъcвaнeтo нa ycтoйчивитe иĸoнoмичecĸи вpъзĸи чpeз нaлaгaнeтo нa иĸoнoмичecĸи caнĸции и зaдълбoчaвaнeтo нa иĸoнoмичecĸия cпaд, ĸaтo ce ocтaвят нeзaceгнaти caмo oпpeдeлeни „зoни нa иĸoнoмичecĸи pacтeж”. Зa oтбeлязвaнe e, чe пpeз Πъpвaтa и Bтopaтa cвeтoвни вoйни зa тaĸaвa зoнa бeшe oпpeдeлeнa Aмepиĸa. И тъй ĸaтo тoгaвa тoвa ce xapeca нa aмepиĸaнцитe, ceгa тe oтнoвo пpaвят oпит дa пoвтapят тoзи cцeнapий. Πapaлeлнo c тoвa, ĸaĸтo e извecтнo, eднa oт цeлитe нa вcяĸa cвeтoвнa вoйнa e „зaнyлявaнe” или нaй-мaлĸo oбeзцeнявaнe нa дoвoeннитe дългoвe и pecтapтиpaнe нa cвeтoвнaтa иĸoнoмиĸa . Eдвa ли e нeoбxoдимo дa гaдaeм ĸoя cтpaнa щe бъдe нaй-oблaгoдeтeлcтвaнa oт тoвa, cлeд ĸaтo дългoвeтe нa CAЩ вeчe нaдxвъpлят 19 тpилиoнa дoлapa. Tyĸ вeднaгa възниĸвa въпpocът: Koи ca вepoятнитe зoни нa вoeнeн ĸoнфлиĸт, cлeд ĸoйтo мoжe дa бъдe pecтapтиpaнa cвeтoвнaтa иĸoнoмиĸa, пpи зaпaзвaнe нa cъщecтвyвaщия либepaлeн иĸoнoмичecĸи мoдeл и ycтaнoвeнaтa пo cтълбицaтa нa „xpaнитeлнaтa вepигa” йepapxия нa ceгaшния финaнcoв eлит? Bapиaнтитe нe ca мнoгo. Ho cпopeд мнeниeтo нa peдицa зaпaдни aнaлизaтopи, нaй-пpиeмливият oт тяx e ĸoличecтвoтo oт нaтpyпaни ĸъм мoмeнтa пpoтивopeчия, дa бъдe пoгpeбaнo въpxy pyинитe нa Pycия . Toгaвa Eвpoпa и Aзия щe ce пpeвъpнaт в тepитopии oт paзбити и дъpжaни в cъcтoяниe нa yпaдъĸ иĸoнoмичecĸи aнĸлaви, oбгpaдeни oт бyшyвaщия cлeд yнищoжaвaнeтo нa Pycия xaoc . Oт eднa cтpaнa, peaлизaциятa нa тoзи cцeнapий щe пoзвoли нa Aмepиĸa дa ce зaпaзи ĸaтo ocтpoв нa cтaбилнocттa и oтпpaвнa тoчĸa зa pecтapтиpaнe нa нoвия pacтeж нa иĸoнoмиĸитe в Eвpoпa и Aзия зa cмeтĸa нa oгpaбeнoтo oт пoбeдeнa Pycия. Oт дpyгa cтpaнa, CAЩ щe мoжe дa зaпaзи xeгeмoниятa cи и щe пpoдължи дa игpae poлятa нa cвeтoвeн жaндapм. Kaĸ oбaчe изглeждa paзпpeдeлeниeтo нa интepecитe в чeтиpиъгълниĸa oт CAЩ, Pycия, Eвpoпa и Kитaй? Яcнo e, чe CAЩ и Pycия ca aнтaгoниcти. Toвa e тaĸa, зaщoтo, ĸaĸтo oтбeлязaxмe и пo-гope, CAЩ мoжe дa зaпaзи ceгaшнaтa cи poля caмo зa cмeтĸa нa yнищoжaвaнeтo нa Pycия и oтcлaбвaнeтo нa Eвpoпa и Kитaй. Aĸo тaзи xипoтeзa e вяpнa, възмoжнocттa зa пocтигaнe нa няĸaĸъв ĸoнceнcyc мeждy Baшингтoн и Mocĸвa cтaвa cъвceм xипoтeтичнa, ocвeн aĸo тoвa нe cтaнe зa cмeтĸa нa чacтичнa зaгyбa нa нaциoнaлeн cyвepeнитeт oт cтpaнa нa Pycия или пълнaтa й ĸaпитyлaция пpeд CAЩ. Toecт, aĸo Pycия иcĸa дa oцeлee и нe ce пoяви няĸoй „нoв Гopбaчoв” тя нeизбeжнo щe тpябвa дa влeзe в cблъcъĸ cъc CAЩ. B тaзи cитyaция Eвpoпa щe бъдe ecтecтвeн cъюзниĸ нa Aмepиĸa. Днeшнoтo вacaлнo пoдчинeниe нa Eвpoпa нa зaпoвeдитe нa Baшингтoн нe ce дължи caмo нa „пoĸyпĸaтa” нa eвpoпeйcĸи пoлитици или нa „ĸoмпpoмaтитe”, c ĸoитo ЦPУ дъpжи ĸлючoви eвpoпeйcĸи лидepи.


Джон Грей: „Комунизмът е само един от западните проекти. Падането на комунизма ще бъде последвано от краха на Западния проект. Комунизмът остави след себе си пустота, която запълниха демоните, които днес господстват в света”

tunnckoCore commented 7 years ago

backup

'use strict'

function freak2ing (options) {
  if (typeof options === 'function') {
    options = { render: options }
  }

  let {
    initialState,
    initialProps,
    effects,
    reducers,
    events,
    render,
  } = options

  reducers = Object.assign({}, reducers)
  effects = Object.assign({}, effects)
  events = Object.assign(
    {
      onError: (err) => {
        throw err
      },
      // signature: (actions)
      onWillMount: Function.prototype,
      // signature: ()
      onDidMount: Function.prototype,

      // signature: (actionName, [param1, param2, param3], actions)
      onAction: Function.prototype,
      // signature: (effectName, actions, [param1, param2, param3])
      onEffect: Function.prototype,
      // signature: (reducerName, actions, [param1, param2, param3])
      onReducer: Function.prototype,

      // signature: (oldState, [param1, param2, param3])
      onWillUpdateState: Function.prototype,
      // signature: (oldState, newState, [param1, param2, param3])
      onDidUpdateState: Function.prototype,

      // signature: (nextPropsWithState, nextState)
      onWillUpdate: Function.prototype,
      // signature: (newPropsWithState, newState)
      onDidUpdate: Function.prototype,

      // signature: (nextPropsWithState, oldPropsWithState)
      onWillUpdateProps: Function.prototype,
      // signature: (newPropsWithState, oldPropsWithState)
      onDidUpdateProps: Function.prototype,

      // signature: (newPropsWithState, newState)
      onWillRender: Function.prototype,
      // signature: (newPropsWithState, newState)
      onDidRender: Function.prototype,
    },
    events
  )

  let _STATE = typeof initialState === 'function'
    ? initialState()
    : Object.assign({}, initialState)

  let _PROPS = typeof initialProps === 'function'
    ? initialProps()
    : Object.assign({}, initialProps)

  let actions = {}
  let _NEXT_STATE = {}

  Object.keys(Object.assign({}, reducers, effects)).forEach((name) => {
    const action = createAction(actions, name)
    actions[name] = action
  })

  function createAction (actions, name) {
    return function _action_ (e) {
      // get action args, without leakage
      var i = arguments.length
      var args = []
      while (i--) {
        args[i] = arguments[i]
      }

      // signature: (actionName, [param1, param2, param3], actions)
      events.onAction(name, args, actions)

      // asynchronous action, i.e. "effect"
      // called on next tick
      if (effects[name]) {
        return Promise.resolve().then(() => {
          // signature: (effectName, actions, [param1, param2, param3])
          events.onEffect(name, actions, args)

          // or signature: (actions, [param1, param2, param3])
          // return effects[name](actions, args)

          // signature: (actions, param1, param2, param3)
          return effects[name].apply(null, [actions].concat(args))
        }, events.onError)
      }

      // sync/async action, i.e. "reducer"
      // called in same tick
      // const promise = new Promise((resolve) => {
      // try {
      // signature: (reducerName, actions, [param1, param2, param3])
      events.onReducer(name, actions, args)

      const oldState = Object.assign({}, _STATE)

      // or signature: (actions, [param1, param2, param3])
      // const newState = reducers[name].apply(null, [actions].concat(args))

      events.onWillUpdateState(oldState, _NEXT_STATE, args)

      // or signature: (actions, param1, param2, param3)
      const partialState = reducers[name].apply(null, [actions].concat(args))

      // signature: (oldState, partialState, [param1, param2, param3])
      // ???
      // events.onStateUpdate(oldState, partialState, args)
      _STATE = Object.assign({}, oldState, partialState)

      // signature: (oldState, newState, [param1, param2, param3])
      events.onDidUpdateState(oldState, _STATE, args)
      return _STATE
      // } catch (err) {
      //   return reject(err)
      // }
      // resolves changed state
      //   resolve(_STATE)
      // })

      // return promise.catch(events.onError)
    }
  }

  let { state, ...prevProps } = Object.assign({}, _PROPS)
  let oldState = _STATE

  const createSimpleComponent = (_render) => {
    // console.log(app.actions)
    events.onWillMount(actions)
    return (incomingProps) => {
      let { state: nextState, ...nextProps } = Object.assign({}, incomingProps)
      nextState = Object.assign({}, nextState)

      let prevState = Object.assign({}, _STATE)
      // signature: (nextProps, nextState)
      events.onWillUpdate(incomingProps, nextState)
      // signature: (prevState, nextState)
      events.onWillUpdateState(prevState, nextState)
      const newState = Object.assign({}, prevState, nextState)
      _NEXT_STATE = _STATE = newState
      // signature: (prevState, newState)
      events.onDidUpdateState(prevState, newState)
      let oldProps = { state: oldState, ...prevProps }
      // signature: (nextPropsWithState, oldPropsWithState)
      events.onWillUpdateProps({ state: nextState, ...nextProps }, oldProps)
      // merge only the props, without the state
      const newProps = Object.assign({ actions }, prevProps, nextProps)
      newProps.state = newState
      // signature: (newPropsWithState, oldPropsWithState)
      events.onDidUpdateProps(newProps, oldProps)
      // signature: (newPropsWithState, newState)
      events.onDidUpdate(newProps, newState)
      // signature: (newPropsWithState, newState)
      events.onWillRender(newProps, newState)
      let result = null
      if (typeof newProps.children === 'function') {
        // signature: (newPropsWithState, newState)
        result = newProps.children(newProps, newState)
      } else {
        // signature: (newPropsWithState, newState)
        result = _render(newProps, newState)
      }
      // signature: (newPropsWithState, newState)
      events.onDidRender(newProps, newState)
      events.onDidMount()
      return result
    }
  }

  return typeof render === 'function'
    ? createSimpleComponent(render)
    : (_render) => createSimpleComponent(_render)
}

const wrapWithState = freak2ing({
  initialState: {
    some: 'initial state',
  },
  reducers: {
    addText: (actions, text) => ({ text }),
    updateInitial: (actions, param1) => ({ some: 'set on willmount' }),
  },
  events: {
    onWillMount: (actions) => {
      console.log('init', actions)
      console.log('calling "updateInitial" reducer')
      actions.updateInitial('he he he')
    },
    // signature: (oldState, nextState, [param1, param2, param3])
    onWillUpdateState: (oldState, nextState) => {
      console.log('will state:', oldState, nextState)
      // sasa
    },
    onDidMount: () => {
      console.log('done')
    },
    // signature: (oldState, newState, [param1, param2, param3])
    onDidUpdateState: (oldState, newState) => {
      console.log('did state:', oldState, newState)
    },

    // signature: (nextPropsWithState, nextState)
    onWillUpdate: (nextPropsWithState, nextState) => {
      // sasa
    },
    // signature: (newPropsWithState, newState)
    onDidUpdate: (newPropsWithState, newState) => {
      // sasa
    },

    // signature: (nextPropsWithState, oldPropsWithState)
    onWillUpdateProps: (nextPropsWithState, oldPropsWithState) => {
      console.log('will props', nextPropsWithState)
    },
    // signature: (newPropsWithState, oldPropsWithState)
    onDidUpdateProps: (newPropsWithState, oldPropsWithState) => {
      console.log('did props', newPropsWithState)
    },
  },
})

const App = wrapWithState(({ actions }) => {
  console.log('some view here')
  console.log('calling "addText" from view... (when render')
  actions.addText('hello world')
  return 'end'
})

// console.log(App())
tunnckoCore commented 7 years ago

not working "freaking" and bel with support for components. Tools: Babel + nodemon

/** @jsx belCreateElement */

'use strict';

var headRegex = /^\n[\s]+/;
var tailRegex = /\n[\s]+$/;

var SVGNS = 'http://www.w3.org/2000/svg';
var XLINKNS = 'http://www.w3.org/1999/xlink';

var BOOL_PROPS = {
  autofocus: 1,
  checked: 1,
  defaultchecked: 1,
  disabled: 1,
  formnovalidate: 1,
  indeterminate: 1,
  readonly: 1,
  required: 1,
  selected: 1,
  willvalidate: 1,
};
var COMMENT_TAG = '!--';
var SVG_TAGS = [
  'svg',
  'altGlyph',
  'altGlyphDef',
  'altGlyphItem',
  'animate',
  'animateColor',
  'animateMotion',
  'animateTransform',
  'circle',
  'clipPath',
  'color-profile',
  'cursor',
  'defs',
  'desc',
  'ellipse',
  'feBlend',
  'feColorMatrix',
  'feComponentTransfer',
  'feComposite',
  'feConvolveMatrix',
  'feDiffuseLighting',
  'feDisplacementMap',
  'feDistantLight',
  'feFlood',
  'feFuncA',
  'feFuncB',
  'feFuncG',
  'feFuncR',
  'feGaussianBlur',
  'feImage',
  'feMerge',
  'feMergeNode',
  'feMorphology',
  'feOffset',
  'fePointLight',
  'feSpecularLighting',
  'feSpotLight',
  'feTile',
  'feTurbulence',
  'filter',
  'font',
  'font-face',
  'font-face-format',
  'font-face-name',
  'font-face-src',
  'font-face-uri',
  'foreignObject',
  'g',
  'glyph',
  'glyphRef',
  'hkern',
  'image',
  'line',
  'linearGradient',
  'marker',
  'mask',
  'metadata',
  'missing-glyph',
  'mpath',
  'path',
  'pattern',
  'polygon',
  'polyline',
  'radialGradient',
  'rect',
  'set',
  'stop',
  'switch',
  'symbol',
  'text',
  'textPath',
  'title',
  'tref',
  'tspan',
  'use',
  'view',
  'vkern',
];

function arrayify (val) {
  if (!val) return [];
  if (Array.isArray(val)) return val;
  return [val];
}

function isObject (val) {
  return val && typeof val === 'object' && !Array.isArray(val);
}

function belCreateElement (tag, props, children) {
  var el = void 0;
  props = isObject(props) ? props : {};
  children = children || props.children;

  // If an svg tag, it needs a namespace
  if (SVG_TAGS.indexOf(tag) !== -1) {
    props.namespace = SVGNS;
  }
  props.children = children;

  // if component, pass props and the children to it
  if (typeof tag === 'function') {
    return tag(props);
  }
  children = arrayify(children);

  var ns = false;
  if (props.namespace) {
    ns = props.namespace;
    delete props.namespace;
  }

  // Create the element
  if (ns) {
    el = document.createElementNS(ns, tag);
  } else if (tag === COMMENT_TAG) {
    return document.createComment(props.comment);
  } else {
    el = document.createElement(tag);
  }

  // Create the properties
  for (var p in props) {
    if (props.hasOwnProperty(p)) {
      var key = p.toLowerCase();
      var val = props[p];
      // Normalize className
      if (key === 'classname') {
        key = 'class';
        p = 'class';
      }
      // The for attribute gets transformed to htmlFor, but we just set as for
      if (p === 'htmlFor') {
        p = 'for';
      }

      // support style as object
      // both `fontSize` and `font-size` work that way
      if (key === 'style' && typeof val === 'object') {
        for (var k in val) {
          el.style[k] = val[k];
        }
        continue;
      }

      // If a property is boolean, set itself to the key
      if (BOOL_PROPS[key]) {
        if (val === 'true') val = key;
        else if (val === 'false') continue;
      }
      // If a property prefers being set directly vs setAttribute
      if (key.slice(0, 2) === 'on') {
        el[p] = val;
      } else {
        if (ns) {
          if (p === 'xlink:href') {
            el.setAttributeNS(XLINKNS, p, val);
          } else if (/^xmlns($|:)/i.test(p)) {
            // skip xmlns definitions
          } else {
            el.setAttributeNS(null, p, val);
          }
        } else {
          if (key !== 'children') {
            el.setAttribute(p, val);
          }
        }
      }
    }
  }
  function appendChild (childs) {
    if (!Array.isArray(childs)) return;
    var hadText = false;
    for (var i = 0, len = childs.length; i < len; i++) {
      var node = childs[i];
      if (Array.isArray(node)) {
        appendChild(node);
        continue;
      }

      if (
        typeof node === 'number' ||
        typeof node === 'boolean' ||
        typeof node === 'function' ||
        node instanceof Date ||
        node instanceof RegExp
      ) {
        node = node.toString();
      }

      var lastChild = el.childNodes[el.childNodes.length - 1];
      if (typeof node === 'string') {
        hadText = true;
        if (lastChild && lastChild.nodeName === '#text') {
          lastChild.nodeValue += node;
        } else {
          node = document.createTextNode(node);
          el.appendChild(node);
          lastChild = node;
        }
        if (i === len - 1) {
          hadText = false;
          var value = lastChild.nodeValue
            .replace(headRegex, '')
            .replace(tailRegex, '');
          if (value !== '') lastChild.nodeValue = value;
          else el.removeChild(lastChild);
        }
      } else if (node && node.nodeType) {
        if (hadText) {
          hadText = false;
          var val = lastChild.nodeValue
            .replace(headRegex, '')
            .replace(tailRegex, '');
          if (val !== '') lastChild.nodeValue = val;
          else el.removeChild(lastChild);
        }
        el.appendChild(node);
      }
    }
  }
  appendChild(children);

  return el;
}

/**
 * EXAMPLE
 */

const Strong = (props) => (
  <strong
    style={{
      fontSize: 22,
      color: 'red',
    }}
    {...props}
  />
);

const freaking = require('./index');
const LinkView = freaking({
  state: {
    welcome: 'Hello',
  },
});

const Link = LinkView(({ state, actions, path, children, ...props }) => (
  <a href={path} {...props}>
    {state.welcome}<Strong>{children}</Strong>
  </a>
));

const el = <Link path="/hello" className="btn btn-link">Charlike</Link>;

console.log(el.outerHTML);
window.close();
tunnckoCore commented 7 years ago

More updates.

"use strict";
/** @jsx mich.h */

function mich(options) {}

const Greet = mich({
  state: {
    title: "Foo Bar",
    greeting: "Hello",
    authorized: true,
    clicked: false
  },
  // default/initial props
  props: {
    foo: "webapp"
  }, // used for components
  effects: {
    // here inside in first argument object we dont have access to "state"
    // rest arguments are passed arguments we this effect action is called
    authorized: ({ actions }) => {
      actions.fetch(true);
    },
    // here we have access to the default props, same like in reducers
    fetch: ({ actions }, bool) => {
      return fetch("api.foobar.com/user").then(({ body }) => {
        return actions.update(bool ? body : { fail: "not authorized" });
      });
    }
  },
  reducers: {
    // here we have access to the default props
    update: ({ state, actions }, data) => {
      const { first, last } = data;
      actions.add(first, last);
    },
    add: ({ state, actions }, firstName, lastName) => ({
      realName: `${state.greeting}, ${firstName} ${lastName}`
    })
    // or return a promise
    // add: ({ state, actions }, firstName, lastName) => {
    //  return Promise.resolve({
    //      realName: `${state.greeting}, ${firstName} ${lastName}`
    //  })
    // }
  },
  lifecycle: {
    beforeStateUpdateOrSuch: ({ state, actions }) => {
      if (state.clicked) {
        actions.authorized();
      }
    },
    error: ({ state, actions }, err) => {
      console.error(err.message || err);
    }
  },
  render: ({ state, actions, ...props }) => {
    return (
      <div className="segment">
        <h1>
          {state.title}! This is a {props.kind || props.foo}
        </h1>
        <p>
          {state.realName}
        </p>
        <button onsubmit={actions.authorized} />
      </div>
    );
  }
});

mich.mount(
  <Greet kind="our website" /*actions={mich.createActions(moreActions)}*/ />
);
tunnckoCore commented 7 years ago

morph fn - virtual dom diffing

'use strict'

const isObject = (val) => val && typeof val === 'object' && !Array.isArray(val)
const isVNode = (val) => isObject(val) && val.VNode === true
const isVNodeText = (val) => isVNode(val) && val.type === 'text'

function morph (left, right) {
  if (!left) {
    return right
  }
  if (!right) {
    return left
  }
  if (right.isSameNode && right.isSameNode(left)) {
    return left
  }
  if (left.tagName !== right.tagName) {
    return right
  }
  const isLeftText = isVNodeText(left)
  const isRightText = isVNodeText(right)
  if (isLeftText || isRightText) {
    return morphText(left, right, { isLeftText, isRightText })
  }
  morphProps(left, right)
  morphChildren(left, right)

  return left
}

function morphText (left, right, { isLeftText, isRightText }) {
  if (isLeftText && isRightText) {
    return left.value === right.value ? left : right
  }

  return isLeftText && !isRightText ? right : left
}

function morphProps (left, right) {
  const props = Object.keys(right.properties)
    .filter(
      (propName) =>
        !left.properties.hasOwnProperty(propName) ||
        left.properties[propName] !== right.properties[propName]
    )
    .map((propName) => ({ propName, propValue: right.properties[propName] }))
    .reduce((memo, { propName, propValue }) => {
      memo[propName] = propValue
      return memo
    }, {})

  Object.keys(left.properties)
    .filter((k) => right.properties.hasOwnProperty(k))
    .forEach((name) => {
      props[name] = right.properties[name]
    })

  left.properties = props
  return left
}

function morphChildren (left, { children }) {
  left.children = children.map((newChild, idx) => {
    if (same(newChild, left.children[idx])) {
      return morph(left.children[idx], newChild)
    }
    // console.log(left.children[idx])
    return morph(newChild, left.children[idx])
  })
}

function same (right, left) {
  if (right.isSameNode) return right.isSameNode(left)
  if (right.properties && right.properties.id) {
    return right.properties.id === left.properties.id
  }
  if (right.tagName !== left.tagName) return false
  if (right.type === 'text') return right.value === left.value
  return false
}

exports.morph = morph
exports.default = morph

// WORKS!
// let a = h('div', { bro: 'ok', beta: 'ss' }, 'Hello World')
// let b = h('div', { bro: 'ok', class: 'barry', zazz: 'qux' }, 'Hello Wood World')

// let c = morph(a, b)
// console.log(c)

// WORKS!
// let el1 = h(
//   'div',
//   { id: 'sasa', orig: 'qux', class: 'haha' },
//   'Hello World Beta'
// )
// let el2 = h('div', { id: 'xx', class: 'zzz' }, [
//   'Hello',
//   h('b', 'world'),
//   'yeah!',
// ])
// let res = morph(el1, el2)

better mich-h:

'use strict'

const NO_SUFFIX = /acit|ex(?:s|g|n|p|$)|rph|ows|mnc|ntw|ine[ch]|zoo|^ord/i

const isString = (val) => typeof val === 'string'
const isObject = (val) => val && typeof val === 'object' && !Array.isArray(val)
const isVNode = (val) => isObject(val) && val.VNode === true
const isVNodeText = (val) => isVNode(val) && val.type === 'text'
const isVNodeElement = (val) => isVNode(val) && val.type === 'element'

const VNode = ({ tagName, properties, children }) => ({
  type: 'element',
  tagName,
  properties,
  children,
  VNode: true,
})

const VText = (value) => ({
  type: 'text',
  value,
  VNode: true,
})

function h (tagName, ...rest) {
  const args = [].concat(...rest)
  const node = VNode({ tagName, properties: {}, children: [] })

  args
    .filter((item) => item != null && typeof item !== 'boolean')
    .forEach((item) => {
      let child = null

      if (isString(item) || typeof item === 'number') {
        child = VText(item)
      }
      if (isVNode(item)) {
        child = item
        item = null
      }
      if (isObject(item)) {
        addProps(node, item)
      }

      if (child) {
        node.children.push(child)
      }
    })

  return node
}

function addProps (node, props) {
  node.properties = Object.keys(props).reduce((memo, name) => {
    const value = props[name]

    if (name === 'style') {
      addStyle(memo, value)
    } else if (name === 'class' || name === 'className') {
      addClass(memo, value)
    } else {
      memo[name] = value
    }

    return memo
  }, {})
}

function addStyle (props, value) {
  if (isObject(value)) {
    Object.keys(value).forEach((k) => {
      const val = value[k]
      const suffix = typeof val === 'number' && NO_SUFFIX.test(k) === false

      props.style = props.style || {}
      props.style[k] = suffix ? val + 'px' : val
    })
  } else {
    // assume string
    // add explicit check if errors come
    props.style = value
  }
}

function addClass (props, value) {
  if (Array.isArray(value)) {
    props.class = value.filter(Boolean).join(' ')
  } else if (isObject(value)) {
    props.class = Object.keys(value)
      .filter((name) => Boolean(value[name]))
      .map((name) => name)
      .join(' ')
  } else {
    // assume string
    // add explicit check if errors come
    props.class = value
  }
}

module.exports = {
  isString,
  isObject,
  h,
  VNode,
  VText,
  isVNode,
  isVNodeText,
  isVNodeElement,
}

render / renderToString

'use strict'

const isObject = (val) => val && typeof val === 'object' && !Array.isArray(val)
const isVNode = (val) => isObject(val) && val.VNode === true
const isVNodeText = (val) => isVNode(val) && val.type === 'text'
const isVNodeElement = (val) => isVNode(val) && val.type === 'element'

function render () {}

function renderToString (tree) {
  let buf = ''
  traverse([].concat(tree), (node) => {
    if (isVNodeText(node)) {
      buf += node.value
      return
    }
    if (!isVNodeElement(node)) return

    buf = `${buf}<${node.tagName}${toAttrs(node.properties)}>`
    buf = `${buf}${renderToString(node.children)}</${node.tagName}>`
  })

  return buf
}

function traverse (tree, cb) {
  if (Array.isArray(tree)) {
    tree.forEach((node) => {
      traverse(cb(node), cb)
    })
  } else if (isVNodeElement(tree)) {
    traverse(tree.children, cb)
  }
  return tree
}

let transform = {
  className: 'class',
  htmlFor: 'for',
  httpEquiv: 'http-equiv',
}

function toAttrs (props) {
  return Object.keys(props).reduce((memo, propName) => {
    const value = props[propName]
    const name = propName in transform ? transform[propName] : propName
    if (value === 'true') {
      return `${memo} ${name}`
    }

    if (value === 'false') {
      return `${memo} ${name} ="false"`
    }

    if (name === 'style' && isObject(value)) {
      const style = Object.keys(value)
        .map((key) => `${key}:${value[key]};`)
        .join('')

      return `${memo} style="${style}"`
    }
    if ((name === 'data' || name === 'dataset') && isObject(value)) {
      const data = Object.keys(value)
        .map((key) => `data-${key}="${value[key]}"`)
        .join(' ')

      return `${memo} ${data}`
    }

    if (name !== 'on' && !name.startsWith('on')) {
      return `${memo} ${name}="${value}"`
    }

    return memo
  }, '')
}

module.exports = { renderToString, render }

most of the nanomorph tests pass, but swap proxy elements fail


let nodeA = html`<li id="aa"></li>`
let placeholderA = html`<div id="aa" data-placeholder=true></div>`
placeholderA.isSameNode = function (el) {
  return el === nodeA
}

let nodeB = html`<li id="bb"></li>`
let placeholderB = html`<div id="bb" data-placeholder=true></div>`
placeholderB.isSameNode = function (el) {
  return el === nodeB
}

let a = html`<ul>${nodeA}${nodeB}</ul>`
console.log('A:', renderToString(a))

let b = html`<ul>${placeholderB}${placeholderA}</ul>`
console.log('B:', renderToString(b))

let c = morph(a, b)
console.log('C:', renderToString(c))
// instead of `C: <ul><li id="aa"></li><li id="bb"></li></ul>`
// should be  `C: <ul><li id="bb"></li><li id="aa"></li></ul>`