Choo: Reusable choo components with chooify

Created on 4 Oct 2016  Â·  15Comments  Â·  Source: choojs/choo

First of all my apologies as this is not an issue. I have read a couple of times this discussion about choo state model, and I started writing a browserify transform for choo. chooify.

I'm just starting so is pretty much a work in progress, most of the features described in the repo are not implemented yet, what is already working is a pretty basic management of .choo files. I rewrote the title example with chooify here so you can have a look (that is actually working).

I though in a browserify transform, so the choo repo is not affected, and I want it to show it here to know your thoughts about it. Any help and/or opinion would be appreciated.

Most helpful comment

Reusable components would be a lot easier to manage if models were _injected_ into views explicitly. It would decouple the models from the views, and shift the abstraction toward a world where views are written to expect a particular interface, not a specific model. The need for a coordinating file like a .choo would then be absorbed into the app and routes definitions, ui components would be views expecting specific model interfaces, business logic modules would only be models, and, finally, we all can stop worrying so much about namespaces.

Bonus: this would make re-use of the same view possible with multiple disparate models, start us on the path to standard model interface definitions, and open the door to exploring the potential in higher order models, which I imagine would be like model-level plugins.

All 15 comments

@YerkoPalma haha, it's all cool! - Feel free to open discussions about things on this repo; we'll just mark it as "discussion" and talk away :grin:

Though personally I wouldn't use this solution, I'm definitely interested in figuring out why you wrote this: do you feel writing stateful choo view + model combinations is too hard? What's triggered you to write this? (and how do you think we can we improve?)

do you feel writing stateful choo view + model combinations is too hard?

Writing components isn't hard, but it can be tedious. bel elements are easy and reusable, but to add a choo model you'll have to write, to exportable elements, the view and the model. That's not actually a big deal, the thing is that with a choo model you are polluting the global app state. Think, for instance, in a calendar component, with a selectedDate data. Handling that in the state would make a global selectedDate, if your app include many of them you'll have a lot of dates in the state. Choo solution is namespacing, but, as you said, that can be hard to handle.
So I would like to have some way to write a component that has it's own local data, without polluting the global state, but that can fire reducers and effects to handle the global state. That would make it super easy to export the calendar component, each one with a local selectedDate, and a clean global state.

What's triggered you to write this? (and how do you think we can we improve?)

  • I think a .choo extension is awesome.
  • I though about a pull request, but the would have been too veinvasive. A transform doesn't affect any existing choo app at all.
  • Is the first time I write a browserify transform, so I wanted to try it :)
  • I'm thinking in use it for some custom templates later.

The help that I would like is to know about your opinions. Fisrt of all, is it clear what I'm trying to do? Do you think is related with the actual state management discussion? Why you wouln't use it?

Also, I'm putting it here, because most of the API design isn't definitive, and may change based on any feedback.

Hmm. I'm agree with the @YerkoPalma and understand his idea. But don't like that syntax of the .choo file.
Why not just plain html? And the model can be set in <script></script>? Syntax highlighting would work then.

counter.choo

<main>
  <h1>${author}</h1>
  <p>${state.count}</p>
  <button onclick=${(e) => send('increment')}></button>
</main>
<script type="choo/javascript">{
  local: {
    author: ''
  },
  effects: {
    increment: (action, state) => ({ count: state.count + 1 })
  }
}</script>

Looks better. Yea script type may looks strange. But it seems that GFM and my sublime theme understands that choo/javascript is javascript syntax. Which is cool trick.

edit: You easily can parse this thing. Put everything before <script> to be view

@YerkoPalma What about nested components? haha. Pure example for such thing are forms (I can share one cool form element/view/component soon). Form is one component, but it may have other components in it? ;d Don't know just some brainstorms.. I'm not react or frontend user/dev.

@yoshuawuyts can you give some advanced usage of choo anyone using it in production, some big apps/users? Just curious to review their code.

edit: And actually one more question to @YerkoPalma, why would we need .choo files? I pretty like the idea of javascript views - it is awesome. Because with such .choo files, we loose some features from current js views, like you can require more views (actually, my folder is called elements/ and other is layouts/) and do more js things before returning the yo/bel template.
I recently built few things with choo and seen how your idea is good in one side, but not in another.

Concerning the type attribute of script tag: I'd suggest to change the value to be more like Mustache/Handlebars/Angular1 name them. The former part usually denotes the format. Maybe: text/choo-template? Or text/choo-view etc.

ryuno, agree, but we loose the highlighting and will need additional plugins. intentionally suggest that type.

@tunnckoCore There are two benefits with .choo files.

  • View and component are exportable in a single file. Normally, if you have a choo component that you want to reuse, you have to require the view (bel element) and the model (js object) separatly.
  • Local scope for component model. When you define a model, it adds global a state. So think about a Form component, with a validation state. The validation state is not global, it refers only to that instance of the component, so if you have another form, used by the same component view, it would have the same validation state, which is not correct. Thats the key with .choo files, you have a local property that you can use to refers to the component local data, and is different between other components, so your global state is cleaner.

Now, as you noticed there is downsides about this approach.

I agree that the syntax might be improved, thats why I'm asking for opinions :). Now I'm not sure if your proposal is better because that syntax is though for some template engine, like handlebars. Es6 template strings are not that powerful, for instance, they don't consider conditional statements, or loop statements, just expression interpolation. But still, is a great feature to have javascript on the views, so maybe is important not to lose it. I though, maybe add package name in comment, and just asume an htmlvar, just as the view arguments are assumed. Let me explain with an example.

/* choo-view granim leaflet*/ <- those two package would be required outside view function

// any js code

html`<main> <- Now use the html function (choo/html or bel) to know what to export
</main>`

that would help with keeping js code in the views, otherwise we would have to create some DSL or use some existing template engine, and that is not a good idea.

About nested component, they are pretty much supported with the actual design. Just add them in the local property.

// form.choo
/* choo-view */
`<form>
${input.view(state, prev, send)}
</form>`

/* choo-model */
{
  local: {
    input: require('input.choo')
  }
}

I believe you can wrap with "`" here to remove this ugly requirement in the view

/* choo-view */
`<form>
${input.view(state, prev, send)}
</form>`

soo i believe you can do

function parseView (view, local) {
  // wrap in a function that require choo/html
  // check if there is any package required
  // require the package after choo/html and before the view function
  return `(function () {
      const html = require('bel'
      const local = ${JSON5.stringify(local)}
      return (function (state, prev, send) {
        return html\`${clean(view)}\`
      }).bind(local)
  })()`
}

notice backticks around ${clean(view)}

though for some template engine, like handlebars. Es6 template strings are not that powerful, for instance, they don't consider conditional statements, or loop statements, just expression interpolation.

It's not a problem. It's the same in anyway. Also ES6 Template strings are excellent and they can do these things.


edit: Yea, i'm agree for the local state feature. But all this for this one thing.

Normally, if you have a choo component that you want to reuse, you have to require the view (bel element) and the model (js object) separatly.

You can do this, another question is how much it is correct. You can wrap your view and pass corresponding model to it. (not clear enough in my mind)

const choo = require('choo')
const formComponent = require('./components/form')

const app = choo()

app.model(formComponent.model)
app.router([
  ['/', formComponent.view]
])

and components/form.js

let model = {
  local: {
    active: false
  },
  effects: {
    toggle: (data, state, send, done) => {
      // not sure what `this` is in that context
      model.local.active = !model.local.active
    }
  }
}

exports.view = (state, prev, send) => {
  let local = model.local
  return html`<main onload=${(e) => send('toggle')}>
    <h1 class="${local.active ? 'active' : ''}">${state.title}</h1>
  </main>`
}
exports.model = model

Mmmmm yeaaa.. it may look a bit verbose, I'm agree but there's always have solutions. It's a matter of thinking and imagination. I absolutely don't like this, but I believe something similar would work.

I would do it in different way, cuz I definitely don't get what's the problem to have model separated. Model and view in one place is walk against SoC and i'll never do that. But anyway.. that's just me.

I can continue with this

components/
- form/
-- view.js
-- model.js

components/form/model.js

let model = {
  local: {
    active: false
  },
  effects: {
    toggle: (state, data, send, done) => {
      // not sure what `this` is in that context
      model.local.active = !model.local.active
    }
  }
}

module.exports = model

components/form/view.js

let html = require('choo/html')
let model = require('./model')

module.exports = (state, prev, send) => {
  let local = model.local
  return html`<main onload=${(e) => send('toggle')}>
    <h1 class="${local.active ? 'active' : ''}">${state.title}</h1>
  </main>`
}

./components/form/index.js

// ./components/form/index.js
exports.model = require('./model')
exports.view = require('./view')
// or install `export-files` and
// module.exports = require('export-files')(__dirname)

client.js

let choo = require('choo')
let form = require('./components/form')
let app = choo()

app.model(form.model)
app.router([
  ['/', form.view]
])

As I said, it's a matter of thinking/working/dreaming. Yea there can have some "extra steps" to write a bit more.. but.. For that we can have automation tools, cli what about choo-cli to scaffold some things easily. What about to think to add some wrapper/helper for such things in the framework?

edit2: also thinking that explicitly using local in the template/view is better and more clear.
edit3: snippets are choo@4

I'll come back with something working (also have few more ideas in additon), but first I need a sleep and few hours university tomorrow.

edit: for now can review this koa + bel app https://github.com/tunnckoCore/koa-blog-system
edit2: @yoshuawuyts, actually, are all model.states merged into one and be passed to each view? I'm seeing this model.namespace thing and maybe I can answer myself with "yes".

Sorry to be the naysayer but I think an opposing viewpoint should be added to this thread 😜

I think one of the main benefits of choo is that it doesn't introduce any new file types (like JSX for example) and doesn't break JavaScript, The Languageâ„¢. The biggest appeal to me for choo is that it works without any need for build/compile steps in the electron environment, has a very simple API, and provides a light but robust conceptual framework for dealing with state and dom (basically react/redux: the good parts).

I also don't see needing to define models and views in separate files as a drawback. In many (most?) cases a view shouldn't need its own local state, and separating concerns like views and state seems logical to me.

Introducing a new filetype seems like a step in the wrong direction to me. There are already some attempts at creating reusable choo components (like choodown) that could be refined without needing to create another filetype and parse/compile step to go with it.

However that said I am a grumpy old man who hates transforms for dynamic languages 👴 . If it works well for you and solves a problem elegantly then go for it -- but I would strongly prefer that a .choo extension not become a core requirement for using choo.

additional comment on the gist here

become a core requirement for using choo.

@ungoldman you maybe missing something here. We are not talking for requirements or adds to the core choo. Just concepts and ideas. I was just commenting the style of chooify and tried to represent that all "components" stuff can be done pretty easy currently. :) All is just a matter of thinkingâ„¢.

Reusable components would be a lot easier to manage if models were _injected_ into views explicitly. It would decouple the models from the views, and shift the abstraction toward a world where views are written to expect a particular interface, not a specific model. The need for a coordinating file like a .choo would then be absorbed into the app and routes definitions, ui components would be views expecting specific model interfaces, business logic modules would only be models, and, finally, we all can stop worrying so much about namespaces.

Bonus: this would make re-use of the same view possible with multiple disparate models, start us on the path to standard model interface definitions, and open the door to exploring the potential in higher order models, which I imagine would be like model-level plugins.

Just want to share some more thoughts.. For someone who like more class-ish components... It's not tested, just an example. I believe it looks good too.

const Link = (link, anchor) => ({
  namespace: 'ahref'
  state: { link, anchor },
  view: (html) => (state, prev, send) => html`
    <a href="${state.link}">${state.anchor}</a>
  `
})

const HelloWorld = (initialState, namespace) => ({
  namespace: typeof namespace === 'string'
    ? namespace
    : '',
  state: initialState && typeof initialState === 'object'
    ? initialState
    : {},

  actions: () => ({

  }),

  reducers: () => ({

  }),

  effects: () => ({
    print: (state, data) => {
      console.log(data.payload)
    }
  }),

  subscriptions: () => ({
    callDog: (send, done) => {
      setInterval(function () {
        var data = { payload: 'dog?', myOtherValue: 1000 }
        send('app:print', data, function (err) {
          if (err) return done(err)
        })
      }, 1000)
    }
  }),

  view: (html) => (state, prev, send) => html`
    <h1>${state.app.welcome}</h1>
    ${Link(state.app.href, state.app.text)}
  `
})

const HelloComponent = HelloWorld({
  href: 'http://i.am.charlike.online',
  text: 'i.am.charlike.online',
  welcome: 'Heya, I am Charlike Mike Reagent!!'
}, 'app')

chooComponent.render(HelloComponent, '#helloApp')

Where chooComponent could be thin wrapper for choo/mount and calling component's methods to add/collect all actions, reducers and etc to the model with some namespace (if defined in the component), view will be passed with html param for more easy access without thinking for imports and requires.

I kinda like it, huh. It seems my mind slooowly slooowly are traveling to components and react-ish shits :D

edit: there are few cool things here for me - 1) power of arrows, 2) purely functional, 3) plain javascript files not some custom ones.

Closing because choo@5 will introduce a new API. See #425 for the merged PR. Thanks!

Was this page helpful?
0 / 5 - 0 ratings