Choo: Using Choo to create reusable widgets?

Created on 3 Nov 2016  Â·  13Comments  Â·  Source: choojs/choo

I wanted to see if I could use choo to create a widget that I can add to any HTML page. I had the following requirements:

  1. Ability to pass in options to customize the created DOM element
  2. Imperatively call methods on the element to trigger actions
  3. Handle events triggered by the element
  4. Have more than once instance of the element on a page

Was choo built to handle this? I noticed that all examples require the router to configured for example. I would not need this and I am interested to know if choo can be used without it. What I would want to use is the model and the view functionality.

I hacked together a POC of how this could be done. You can look at the jsbin here. It works but, as I said it's kind of hacky. My problem was really that I needed to get access to the send function, to be able to attach it to the element instance, in order to be able to call a function on the rendered element. The only way I could think to do it was to expose the send function outside of the main view.

So, is there a better way?

By the way, great framework. It's really fun!

Most helpful comment

We should probably document this a bit further in the handbook, but https://www.npmjs.com/package/nanocomponent is a thing now :sparkles:

All 13 comments

Thanks! - Yeah, I think this should be possible: probably wanna turn off
the href handler and the history API, and then it should be good I think

On Thu, Nov 3, 2016 at 11:30 PM Debbie Facey [email protected]
wrote:

I wanted to see if I could use choo to create a widget that I can add to
any HTML page. I had the following requirements:

  1. Ability to pass in options to customize the created DOM element
  2. Imperatively call methods on the element to trigger actions
  3. Handle events triggered by the element
  4. Have more than once instance of the element on a page

Was choo built to handle this? I noticed that all examples require the
router to configured for example. I would not need this and I am interested
to know if choo can be used without it. What I would want to use is the
model and the view functionality.

I hacked together a POC of how this could be done. You can look at the
jsbin here http://jsbin.com/najuna/edit?html,js,output. It works but,
as I said it's kind of hacky. My problem was really that I needed to get
access to the send function, to be able to attach it to the element
instance, in order to be able to call a function on the rendered element.
The only way I could think to do it was to expose the send function
outside of the main view.

So, is there a better way?

By the way, great framework. It's really fun!

—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
https://github.com/yoshuawuyts/choo/issues/309, or mute the thread
https://github.com/notifications/unsubscribe-auth/ACWlevn4By9f-yS5DDZPUoHsk7WMHP3sks5q6lJ4gaJpZM4Ko8Mc
.

OK. So you saying it possible that future version of choo would not required the router?
What about the ability to utilize the send function outside of a view? Maybe something like app.send().

Oh you can already disable these things; there's no way to take out the
router though but yeah if you just have one route it's basically the same
thing. As for interacting with the outside world: you can pull down events
from outside through a subscription making something like app.send()
unnecessary :D

On Thu, Nov 3, 2016 at 11:58 PM Debbie Facey [email protected]
wrote:

OK. So you saying it possible that future version of choo would not
required the router?
What about the ability to utilize the send function outside of a view?
Maybe something like app.send().

—
You are receiving this because you commented.

Reply to this email directly, view it on GitHub
https://github.com/yoshuawuyts/choo/issues/309#issuecomment-258286820,
or mute the thread
https://github.com/notifications/unsubscribe-auth/ACWlepWPI45cXo-atzbA7alJ6UVpFbUhks5q6lkTgaJpZM4Ko8Mc
.

Hey I did something similar in my app, I have a wrapper utility function that injects child views into a parent view, that way I can separate my components from a parent template view and still use the router. It also allows my setup a pre-route and post route hook. I can also have different parent views for different routes like different layouts if i want.
util.js

export const loadPage= (parent:Function,child:any)=>(...args:any)=>{
//pre-route function here, post route can be added as callbacks in the args
  return parent(...args,child(...args))
}

main.js

export function main (state, prev, send,child)
{
return html '<div>${child}</div>'
}

child.js

export const child=function(state,prev,send){
return html '<div>I am a child</div>'
}

app.js

import {child} from "child.js"
import {main} from "main.js"
import {loaPage} from "util.js"
const app= require("choo")()

app.router("/",(route) => [
  route('/',loadPage(main,child))
]
const tree = app.start()
document.body.appendChild(tree)

@rollymaduk. That's a nice pattern, but doesn't really answer my question. I want to be able to essentially use a choo created element outside of a choo application. Just basically drop it on any page and be able to communicate with it via both handling events triggered from within the element as well as manually triggering actions. It is the latter that I asking about.

@yoshuawuyts I think I need to clarify a bit more.
You say I can pull events from outside world. That is fine, but what if I want to push data to the widget?

i.e. What if I want to trigger an action programmatically from outside? In the code snippet below, I've exposed an incr function on the element to do just that. It's just not very clean.
As you see in the mainView function, i'm creating a closure around the send parameter by creating a increment function that calls it, and then assigning the increment function to a variable outside of the function. This way I can then add the increment function to the element instance after it is created: const tree = app.start(); tree.incr = increment;

As I said, it works but not very clean. Just wondered if there is a better way.
I guess it's not a popular use case though, so I can just work with the hack for now.

function counterElement(id, initialCount) {
  const app = choo()

  let increment = null;
  let decrement = null;

  app.model({
    namespace: 'counter',
    effects: {
      triggerIncrement: (data, state, send, done) => {  
        document.getElementById(id).dispatchEvent(new Event('incr'));
        send('counter:increment', null, done);
      },
      triggerDecrement: (data, state, send, done) => {       
        document.getElementById(id).dispatchEvent(new Event('decr'));
        send('counter:decrement', null, done);
      }
    },
    state: {
      count: initialCount || 0
    },
    reducers: {
      increment: (data, state) => ({
        count: state.count + 1
      }),
      decrement: (data, state) => ({
        count: state.count - 1
      })
    }
  })

  const mainView = (state, prev, send) => {

    increment = function(e) { send('counter:increment'); }
    decrement = function(e) { send('counter:increment'); }

     return counter({
       count: state.counter.count, 
       incr: (e) => send('counter:triggerIncrement'),
       decr: (e) => send('counter:triggerDecrement')
     })    
  };

  const counter = (opts) => {
    return html`
    <div id="${id}">
         Count: ${opts.count || 0}
        <button onclick=${opts.incr}>+</button>
        <button onclick=${opts.decr}>-</button>
    </div>
  `
  }

  app.router((route) => [
    route('/', mainView)
  ])

  const tree = app.start()

  tree.incr = increment;
  tree.decr = decrement;

  return tree;
}

let el = counterElement("counter1", 5);
document.body.appendChild(el);

let el2 = counterElement("counter2", 20);
document.body.appendChild(el2);

el.incr()
el.incr()

el.addEventListener('incr', function (e) { alert('You incremented! Yay!') }, false);

I see what you mean, perhaps in order to create a more generic send you could just avoid the closure and expose the send from your main view assigned to a variable on your returned tree element... havent tried it but I suspect it might work.

function counterElement(id, initialCount) {
  const app = choo()

  let send= null;

  app.model({
    namespace: 'counter',
    effects: {
      triggerIncrement: (data, state, send, done) => {  
        document.getElementById(id).dispatchEvent(new Event('incr'));
        send('counter:increment', null, done);
      },
      triggerDecrement: (data, state, send, done) => {       
        document.getElementById(id).dispatchEvent(new Event('decr'));
        send('counter:decrement', null, done);
      }
    },
    state: {
      count: initialCount || 0
    },
    reducers: {
      increment: (data, state) => ({
        count: state.count + 1
      }),
      decrement: (data, state) => ({
        count: state.count - 1
      })
    }
  })

  const mainView = (state, prev, send) => {

    send= send

     return counter({
       count: state.counter.count, 
       incr: (e) => send('counter:triggerIncrement'),
       decr: (e) => send('counter:triggerDecrement')
     })    
  };

  const counter = (opts) => {
    return html`
    <div id="${id}">
         Count: ${opts.count || 0}
        <button onclick=${opts.incr}>+</button>
        <button onclick=${opts.decr}>-</button>
    </div>
  `
  }

  app.router((route) => [
    route('/', mainView)
  ])

  const tree = app.start()

  tree.send= send;

  return tree;
}

let el = counterElement("counter1", 5);
document.body.appendChild(el);

let el2 = counterElement("counter2", 20);
document.body.appendChild(el2);

el.send('counter:increment')
el.send('counter:triggerIncrement')

el.addEventListener('incr', function (e) { alert('You incremented! Yay!') }, false);

@rollymaduk Thanks so much! This fits my needs exactly. At a glance, it appears the API may have changed slightly since you posted this, so in case anyone else is interested I've added an example that works for me with 4.0.1 here

The only other note is that href: false seems necessary to prevent the router from failing. I'll update the example as I clean it up a bit more…

Were you able to resolve this?

@yoshuawuyts was that a question for me or in response to the above? I hate to hijack the issue with my concerns simply because I was the last to comment. 😄

Edit: Rereading, yeah, just checking with the original poster for closability. Describing the below since after trying a couple different approaches, I just deleted what I didn't need from choo and found the remaining part to be concise and useful for a self-contained widget! Probably the way I'd do it if I hadn't run into the other snag.

FWIW though, my goal with choo is to recreate a control widget more modern and feature-complete than dat.gui and perhaps a bit too divergent from control-panel to simply keep building on that. React is too big; plain JS is too ad-hoc. Choo seems perfect.

Taking a slightly different tack, I got as far as distilling choo down to choo-lite.js and built this.

I got hung up on this yo-yo issue in which what seems like a weird browser quirk negates some of the simplicity for which I was aiming. Haven't yet figured out how to work around it, but everything else about the approach works for me!

Yay, glad to hear! Re: your issue - we're currently working on nanomorph;
perhaps that yields a better result?
https://github.com/yoshuawuyts/nanomorph

On Sun, Jan 8, 2017 at 8:20 PM Ricky Reusser notifications@github.com
wrote:

@yoshuawuyts https://github.com/yoshuawuyts was that a question for me
or a general question? I hate to hijack the issue with my concerns simply
because I was the last to comment. 😄

FWIW though, my goal with choo is to recreate a control widget more modern
and feature-complete than dat.gui
https://workshop.chromeexperiments.com/examples/gui/#1--Basic-Usage and
perhaps a bit too divergent from control-panel
https://github.com/freeman-lab/control-panel/ to simply keep building
on that. React is too big; plain JS is too ad-hoc. Choo seems perfect.

Taking a slightly different tack, I got as far as distilling choo down to
choo-lite.js
https://github.com/rreusser/control-panel-2/blob/master/src/choo-lite.js
and built this http://rreusser.github.io/control-panel-2/.

I got hung up on this issue https://github.com/maxogden/yo-yo/issues/56
in which what seems like a weird browser quirk negates some of the
simplicity for which I was aiming. Haven't yet figured out how to work
around it, but everything else about the approach works for me!

—
You are receiving this because you were mentioned.

Reply to this email directly, view it on GitHub
https://github.com/yoshuawuyts/choo/issues/309#issuecomment-271172369,
or mute the thread
https://github.com/notifications/unsubscribe-auth/ACWleqJSwuHr6CbjzJCTXUrq9Bsz3jQZks5rQTbogaJpZM4Ko8Mc
.

Although super early stages, this might be useful.

https://github.com/ohgoodlord/choo-component

Suggestions welcome!

Nice work! Consider adding this to the awesome-choo repo ✨

On Sat, Jan 21, 2017, 11:02 Joseph Luck notifications@github.com wrote:

Although super early stages, this might be useful.

https://github.com/ohgoodlord/choo-component

Suggestions welcome!

—
You are receiving this because you were mentioned.

Reply to this email directly, view it on GitHub
https://github.com/yoshuawuyts/choo/issues/309#issuecomment-274252025,
or mute the thread
https://github.com/notifications/unsubscribe-auth/ACWlegKEPP-w23iIPWmPrMv0fXgyt6iKks5rUde2gaJpZM4Ko8Mc
.

We should probably document this a bit further in the handbook, but https://www.npmjs.com/package/nanocomponent is a thing now :sparkles:

Was this page helpful?
0 / 5 - 0 ratings