choojs / nanohtml

:dragon: HTML template strings for the Browser with support for Server Side Rendering in Node.
MIT License
686 stars 49 forks source link

[feature] add thunking API #40

Open yoshuawuyts opened 8 years ago

yoshuawuyts commented 8 years ago

In yo-yo and choo it's a best practice to have element functions in an application return the same element given the same input. This mechanism is commonly referred to as thunking.

How would we feel about adding a light wrapper that exposes a thunk function so that elements can be thunked with little effort. I was thinking of an API along these lines:

// elements/my-button.js
const thunk = require('bel/thunk')
const html = require('bel')

module.exports = thunk(createMyButton)

// create a cool button that has a different color
function createMyButton (color) {
  return html`
    <button style="background-color: ${color}">
      Click me! :D
    </button>
  `
}

In a choo app this could then be consumed as:

const html = require('choo/html')
const choo = require('choo')

const myButton = require('./elements/my-button')

const app = choo()
app.router([ '/', myView ])

app.model({
  state: { color: 'blue' }
})

const tree = app.start()
document.body.appendChild(tree)

function myView (state, prev, send) {
  return html`
    <main>
      ${myButton(state.color)}
    </main>
  `
}

What do you think? Would this be a useful addition to bel, or should we continue to point people to use an external thing / write it themselves? I was thinking of including this into choo, but given that we strongly encourage people to use bel for standalone components (yay, reusability) I figured it might make more sense to include it here first, and then have it propagate upwards through yo-yo to choo. Thanks!

See Also

shama commented 8 years ago

Hmm let me give it some more thought. Initially it feels like this should be in the diffing library or yo-yo as bel doesn't know about previous elements.

timwis commented 8 years ago

Yeah, doesn't thunking only become relevant when you're executing the element creation over and over again (a la morphdom)?

yoshuawuyts commented 8 years ago

@timwis yeah you're right, but this type of rendering is quickly becoming the default; e.g.

Probably the only hot new one around that doesn't do virtual-dom diffing would be Angular 2, but that's about it.

shama commented 8 years ago

Just an update that I've been experimenting with ideas but nothing really to show for yet. But wanted to mention using a feature like this for handling <canvas> updates.

Currently <canvas> tags don't work well in the idiomatic flow because the tag never indicates itself as changed. The simple fix would be to always assume a <canvas> has changed when morphing, replacing with the new one.

But it would interesting if we let the element author choose when to invalidate. Basically the thunk just remembers the last passed arguments/element and appends to the function. Then any element not marked or first time through the thunk, gets updated. Any previous that are marked will always be ignored:

const html = require('yo-yo')
const thunk = require('yo-yo/thunk')

/* prev arg gets added by thunk to the end of the function call */
module.exports = thunk(function createCanvas (data, prev) {
  let canvas = prev.element
  if (prev.args[0].width !== data.width || prev.args[0].height !== data.height) {
    canvas = html`<canvas width="${data.width}" height="${data.height}" />`
  }
  const ctx = canvas.getContext('2d')
  ctx.fillRect(data.x, data.y, data.width, data.height)
  return canvas
})

/* ... */

let data = {x:0,y:0,width:100,height:100}
raf(function loop () {
  data.x += .1
  if (data.x > 100) data.width *= 2
  yo.update(root, canvas(data))
  raf(loop)
})

This might simplify the validation check too as we wouldn't need to traverse objects and arrays checking if the arguments have changed. We just let the element author choose the parameters that decide that. Which most of the time would be 1 or 2 if statements.

Or we could provide that API for convenience to the author to apply as needed:

const html = require('yo-yo')
const thunk = require('yo-yo/thunk')
module.exports = thunk(function createMyButton (data, prev) {
  let button = prev.element
  if (!button || prev.hasChanged(data)) {
    button = html`<button>${data.label}</button>`
  }
  return button
})
timwis commented 8 years ago

That looks awesome @shama! Thanks for the writeup. prev being an object with element and args properties (rather than just the previous version of data) was confusing at first but I don't have a better idea. Maybe if it were called cache or something...

If I recall correctly, the way react (or at least redux) handles this is by "mapping state to props" when connecting the store to the component, so that react knows whether to update that specific component on a given state change. Right?

roobie commented 7 years ago

@yoshuawuyts https://github.com/yoshuawuyts/cache-element/ can be used to thunk element, or am I confused?

yoshuawuyts commented 7 years ago

Yup, though I recommended nanocomponent these days - iterated on the interface a bit

On Mon, Feb 6, 2017, 23:15 Björn Roberg notifications@github.com wrote:

@yoshuawuyts https://github.com/yoshuawuyts https://github.com/yoshuawuyts/cache-element/ can be used to thunk element, or am I confused?

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/shama/bel/issues/40#issuecomment-277831819, or mute the thread https://github.com/notifications/unsubscribe-auth/ACWlerG7SCfNAjUvrJ1sYvee_HIQ_er7ks5rZ5uTgaJpZM4Jbsbi .