anthonyshort / deku

Render interfaces using pure functions and virtual DOM
https://github.com/anthonyshort/deku/tree/master/docs
3.42k stars 130 forks source link

Sources #178

Closed joshrtay closed 9 years ago

joshrtay commented 9 years ago

@dominicbarnes comment about extracting sources from propTypes got me thinking about enhancing sources to allow for namespaces. The problem with sources as they stand is that they can only be added at the top level, so it's really impossible to make self contained sub apps that compose. What I'd really like to be able to do is assemble large client side apps the same I assemble express apps, just mount the sub apps at various endpoints and provide them with a render function.

/**
 * app.js
 */

export const sources = {
 page: 'currentPage' 
}

export function render ({props}) {
 return (
   <div class='App'>
     <div class='App-body'>{props.page}</div>
   </div>
 )
}

/**
 * routes.js
 */

import router from 'hypothetical/router.js'
import dashboard from './dashboard'
import profile from './profile'

export default routes

function routes (app) {
  let r = router()
  r.use(function(ctx, next) {
    ctx.render = function(component) {
      app.set('currentPage', component)
    }
  })

  // dashboard and profile are sub apps that
  // handle their own subrouting
  r.use('/dasboard', dashboard)
  r.use('/profile', profile)
}

/**
 * index.js
 */

import App from './app'
import routes from './routes'

deku(<App/>).use(routes)

With this setup it would be preferable if sub apps had self contained or inherited sources to avoid naming collisions. We could achieve this by making sources work a bit more like context in react. Instead of being restricted to setting sources at the root, all components can set sources that their children can then request. This could be implemented by letting components set child sources or by letting deku apps compose.

App as component (option 1)

Deku apps are themselves components.

import {deku, render} from 'dekujs/deku'

let C = {
  sources: {name: 'currentName'},
  render: function ({props}) {
    return <div>My name is: {props.name}</div>;
  }
}

let B = deku({
  render: function() {
    return <C/>
  }
})

let A = deku({
  render: function() {
    return <B/>
  }
})

// Output: "My name is:"
render(<A/>, document.body)

// Output: "My name is: Tio"
A.set('currentName', 'Tio')

// Output: "My name is: Elliot"
B.set('currentName', 'Elliot')

// Output: "My name is: Elliot"
A.set('currentName', 'Tio')

Sources as context (option 2)

This approach is similar to the context api in react.

import {store, render} from 'dekujs/deku'
let storeB = store()
let storeA = store()

let C = {
  sources: {name: 'currentName'},
  render: function ({props}) {
    return <div>My name is: {props.name}</div>;
  }
}

let B = {
  getChildSources: function () {
    return storeB
  },
  render: function() {
    return <C/>
  }
}

let A = {
  getChildSources: function () {
    return storeA
  },
  render: function() {
    return <B/>
  }
}

// Output: "My name is:"
render(<A/>, document.body)

// Output: "My name is: Tio"
storeA.set('currentName', 'Tio')

// Output: "My name is: Elliot"
storeB.set('currentName', 'Elliot')

// Output: "My name is: Elliot"
storeA.set('currentName', 'Tio')
anthonyshort commented 9 years ago

I really like those ideas. I think I'm leaning more towards context like React, but we will need to update the way the dirty check works for components.

Having the apps as components is interesting... If we went the context route we actually wouldn't even need the app concept and just render vnodes directly (since the app is really just for setting data on):

render(
  document.body,
  <div>Hello!</div>
)

Which we can allow currying with so we can just do:

let renderBody = render(document.body)
renderBody(<div>Hello!</div>)
renderBody(<span>World</span>)

This would mean we wouldn't need to do something like option 1 if the tree is gone. I can't think of a really nice way of doing context that doesn't seem like magic or global variables. There could be the case where some deeply nested component needs some context value set at the top-level but then you'd forget to set it at some intermediate level.

So we'd need to decide if that magic and scope is more valuable than just have a single "environment" context like we do now.

I'm +1 for option 2 if we can figure it out nicely. With sources, validation, and potentially the vdom creation out of core we'd have a much simpler library that other tools could build on top of.

anthonyshort commented 9 years ago

@lancejpollard you'll be interested in this thread :)

joshrtay commented 9 years ago

New idea for sources:

If sources are just observables and initalState can optionally return an observable then sources can be implemented really easily and generally. If you wanted to use flyd it might look something like:

import {render} from 'dekujs/deku'
import {stream} from 'paldepind/flyd`

let B = {
  initialState: function ({context}) {
    return stream([context.currentUser], function() {
      return {name: context.currentUser()}
   })
  },
  render: function ({state}) {
    return <div>My name is: {state.name}</div>;
  }
}

let currentUser = stream()
let A = {
  context: {
   return {currentUser: currentUser}
  },
  render: function() {
    return <B/>
  }
}

// Output: "My name is:"
render(<A/>, document.body)

// Output: "My name is: Tio"
currentUser('Tio')
joshrtay commented 9 years ago

@lancejpollard Just realized if props is a stream then props and state can safely be merged #144. Although it does significantly change the API.

import {stream} from 'paldepind/flyd'
export function initialState({props}) {
  return stream([props], function() {
    return {
      open: props().open, 
      onCancel: props().onCancel, 
      onConfirm: props().onConfirm
    }
 })
}

export function render({state}) {
  let {onCancel, onConfirm, open} = state
  if (!open) return <noscript></noscript>

  return (
    <div class='Dialog'>
      <div class='Dialog-title'>Confirm?</div>
      <div class='Dialog-button' onClick={cancel}>Cancel</div>
      <div class='Dialog-button' onClick={confirm}>Confirm</div>
    </div>
  );

  function cancel() {
    setState({ open: false })
    onCancel()
  }

  function confirm() {
    setState({ open: false })
    onConfirm()
  }
}