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

Finally, done #1

Open
tunnckoCore opened this issue Aug 3, 2017 · 3 comments
Open

Finally, done #1

tunnckoCore opened this issue Aug 3, 2017 · 3 comments

Comments

@tunnckoCore
Copy link
Member

tunnckoCore commented Aug 3, 2017

Around 1.81kb min+gzip - without the diffing and html/jsx thing. You can choose how to work under the hood - with virtual dom or not. So if you choose virtual dom and jsx, then from the views return jsx, and pass Virtual dom diffing algorithm as app.diff(oldTree, newTree).

Otherwise, good combo is bel plus nanomorph but minified and gzipped with them is ~5.63kb

Main Parts:

index.js

const dush = require('dush')
const dushRouter = require('./dush-router-v2')
const dushChanges = require('./dush-changes')
const dushActions = require('./dush-actions')

/* eslint-disable prefer-destructuring */

const HISTORY_OBJECT = {}

module.exports = function Chika (model, _context) {
  _context = {}

  /**
   * Some internal plugins
   */
  const app = dush()
    .use(dushRouter())
    .use(dushChanges())
    .use(dushActions(model))

  let _tree = null

  app.off('route')
  app.on('route', (view, ctx) => {
    ctx = Object.assign({}, ctx, {
      state: app.state,
      actions: app.actions
    })

    const { state, actions, params, pathname, route } = ctx
    const match = { params, pathname, route }
    _context = { state, actions, params, match }

    const newTree = view(_context, _context.params, _context.match)

    // each route will have signature: ({ state, actions }, params, match)
    // so it is easy to destruct `params` and `match`
    return (_tree = app.diff(_tree, newTree))
  })

  /**
   * Re-render when state get updated
   */
  app.on('stateUpdate', (currentState, oldState) => {
    const view = app.getRoutes(_context.match.route)
    _context.state = currentState

    _tree = app.diff(_tree, view(_context, _context.params, _context.match))
  })

  /**
   * Start the application and get generated dom tree,
   * which you should append somewhere in the dom
   */
  app.start = () => {
    _tree = app.emit(window.location.pathname, app.state)

    app.on('render', (node, hist) => {
      if (hist) {
        window.history.replaceState(HISTORY_OBJECT, '', node.pathname)
      } else {
        window.history.pushState(HISTORY_OBJECT, '', node.pathname)
      }
      app.emit(node.pathname, app.state)
    })

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

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

    return _tree
  }

  return app
}
@tunnckoCore
Copy link
Member Author

tunnckoCore commented Aug 3, 2017

router.js (move it as v2 of dush-router)

const PREFIX_FOR_ROUTES = 'DUSH__ROUTES__'

module.exports = () => (app) => {
  app.on('route', (view, context) => view(context))

  let el = null
  const { on, emit } = app

  app.on = (route, fn) => {
    if (route.charAt(0) === '/') {
      const handler = app.createRoute(route, fn)
      on(`${PREFIX_FOR_ROUTES}${route}`, handler)
    } else {
      on(route, fn)
    }
  }

  // eslint-disable-next-line max-params
  app.emit = (...args) => {
    if (args[0].charAt(0) === '/') {
      return (el = navigate(...args))
    } else {
      // dush: max 3 args are supported to be emitted
      emit(...args)
    }
  }

  app.createRoute = (route, fn) => {
    const res = makeRoute(route)
    const r = Object.assign({ route, fn }, res)

    r.matcher = (fp) => (r.regex.test(fp) ? collectParams(r, fp) : 0)

    // eslint-disable-next-line
    return Object.assign((...args) => fn(...args), r)
  }

  function navigate (pathname, state) {
    let found = 0
    const routes = app.getRoutes()

    /* eslint-disable fp/no-loops */
    for (let name in routes) {
      if (found || !routes[name]) {
        break
      }

      /* eslint-disable prefer-destructuring */
      const route = routes[name]
      const params = route.matcher(pathname)

      // if `0` -> no match and no params
      // if `{}` -> no params
      if (!params) {
        continue
      }
      found = 1

      const context = Object.assign({}, route, { state, params, pathname })

      emit('route', (...args) => (el = route.fn(...args)), context, el)
    }
    if (!found) {
      emit('notFound', { state, pathname })
    }
    return el
  }

  // get route object from:
  // - routePath, e.g. /users/:user/photos/:photoId
  // - pathname, e.g. /users/charlike/photos/345345
  // - or get all routes
  //
  // returns object { [routePath]: Object }
  app.getRoutes = (name) => {
    name = name || 0
    let routes = {}

    Object.keys(app._allEvents)
      .filter((routeName) => routeName.startsWith(PREFIX_FOR_ROUTES))
      .forEach((route) => {
        const routeName = route.slice(PREFIX_FOR_ROUTES.length)
        const routeObject = app._allEvents[route][0] // eslint-disable-line

        if (!name) {
          routes[routeName] = routeObject || 0
        }
        if (name && (name.indexOf(':') > 0 || routeObject.matcher(name))) {
          // eslint-disable-next-line
          routes = routeObject
        }
      })

    return routes
  }
}

function makeRoute (route) {
  let keys = []
  let re = route.replace(/\//g, '\\/').replace(/:(\w+)/g, (_, name) => {
    keys.push(name)
    return '(\\w+)'
  })
  let regex = `^${re}$`

  return {
    regex: new RegExp(regex, 'i'),
    keys: keys
  }
}

function collectParams (r, pathname) {
  r.params = {}
  pathname.replace(r.regex, (...args) => {
    args.slice(1, -2).map((val, i) => {
      r.params[r.keys[i]] = val
    })
  })
  return r.params
}

@tunnckoCore
Copy link
Member Author

browser-router.js - handling hrefChange and historyChange (popstate)

module.exports = () => (app) => {
  onHistory((node, e) => {
    app.emit('historyChange', node, e)
  })
  onHref((node, e) => {
    app.emit('hrefChange', node, e)
  })
}

/**
 * UTILS
 */

function onHistory (func) {
  window.addEventListener('popstate', function onpopstate (e) {
    // fails, probably because can't understand `window`
    // eslint-disable-next-line
    const { pathname, search, href, hash } = window.location
    func({ pathname, search, href, hash }, e)
  })
}

function onHref (func) {
  window.addEventListener('click', function onclick (e) {
    if (which(e) !== 1) {
      return
    }
    if (
      e.metaKey ||
      e.ctrlKey ||
      e.shiftKey ||
      e.altKey ||
      e.defaultPrevented
    ) {
      return
    }

    // ensure link
    // use shadow dom when available
    /* eslint-disable fp/no-loops, prefer-destructuring */
    let el = e.path ? e.path[0] : e.target
    while (el && el.nodeName !== 'A') {
      el = el.parentNode
    }

    if (!el || el.nodeName !== 'A') {
      return
    }

    // allow mailto links to work normally
    const link = el.getAttribute('href')
    if (link && link.indexOf('mailto:') > -1) {
      return
    }

    // allow external links to work normally
    const sameHost = el.host === window.location.host
    if (!sameHost) {
      return
    }

    // prevent default behaviour on internal links
    if (sameHost || el.pathname === window.location.pathname) {
      e.preventDefault()
    }

    // allow opt out when custom attribute
    if (!el.hasAttribute('data-no-routing')) {
      func(el, e)
      // eslint-disable-next-line
      return
    }
  })
}

function which (e) {
  e = e || window.event
  return e.which === null ? e.button : e.which
}

@tunnckoCore
Copy link
Member Author

tunnckoCore commented Aug 3, 2017

Example 1 (bel + nanomorph)

const html = require('bel/browser')
const morph = require('nanomorph')
const sleep = require('delay')

// ==================

const App = require('./lib/chika-app')

const app = App({
  state: { title: 'Hello nigga here, Initial!!1!', count: 0 },
  effects: {
    async add ({ actions }) {
      actions.updateCount()
      await sleep(1000)
      actions.updateCount(2)
    }
  },
  reducers: {
    updateCount: ({ state, actions }, num) => ({ 
      count: state.count + (num || 1)
    })
  }
})

/**
 * Always set error listener
 **/
app.once('error', (err) => {
  console.error('ERR!', err)
})

/**
 * Add diffing algorithm as `app.diff`
 **/
app.use(() => {
  app.diff = (oldTree, newTree) => morph(oldTree, newTree)
  return app
})

/**
 * Define your routes, just like event listeners,
 * but starting with `/` always.
 **/

app.on(
  '/',
  ({ state }) => html`<div>
    <h1>home</h1><h2>${state.title}</h2>
  </div>`
)

app.on(
  '/about',
  ({ state }) => html`<div><h1>about</h1><h2>${state.title}</h2></div>`
)

app.on(
  '/users/:user',
  ({ state }, { user }) => html`<div>
    <h1>user</h1>
    <h2>${user}</h2>
    <h3>${state.title}</h3>
  </div>`
)

app.on(
  '/groups/:group/users/:user/edit',
  ({ state }, params) => html`<div>
    <h1>edit user from group ${params.group}</h1>
    <h2>${params.user}</h2>
    <h3>${state.title}</h3>
  </div>`
)

app.on(
  '/users/:user/edit',
  ({ state, params, actions }, p) => html`<div>
    <h1 id="clickme">user edit, cnt: ${state.count}</h1>
    <h2>':user' is: ${JSON.stringify(params)}</h2>
    <h2>'pams': ${JSON.stringify(p)}</h2>
    <h3>'state.title' is: ${state.title}</h3>
    <button onclick=${actions.add}>add +1</button>
  </div>`
)

const tree = app.start()

const main = document.querySelector('#app')
main.appendChild(tree)

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

1 participant