:steam_locomotive::train: - sturdy 4kb frontend framework

Overview

Choo

๐Ÿš‚ ๐Ÿš‹ ๐Ÿš‹ ๐Ÿš‹ ๐Ÿš‹ ๐Ÿš‹
Fun functional programming
A 4kb framework for creating sturdy frontend applications

The little framework that could. Built with โค๏ธŽ by Yoshua Wuyts and contributors

Table of Contents

Features

  • minimal size: weighing 4kb, Choo is a tiny little framework
  • event based: our performant event system makes writing apps easy
  • small api: with only 6 methods there's not much to learn
  • minimal tooling: built for the cutting edge browserify compiler
  • isomorphic: renders seamlessly in both Node and browsers
  • very cute: choo choo!

Example

var html = require('choo/html')
var devtools = require('choo-devtools')
var choo = require('choo')

var app = choo()
app.use(devtools())
app.use(countStore)
app.route('/', mainView)
app.mount('body')

function mainView (state, emit) {
  return html`
    <body>
      <h1>count is ${state.count}</h1>
      <button onclick=${onclick}>Increment</button>
    </body>
  `

  function onclick () {
    emit('increment', 1)
  }
}

function countStore (state, emitter) {
  state.count = 0
  emitter.on('increment', function (count) {
    state.count += count
    emitter.emit('render')
  })
}

Want to see more examples? Check out the Choo handbook.

Philosophy

We believe programming should be fun and light, not stern and stressful. It's cool to be cute; using serious words without explaining them doesn't make for better results - if anything it scares people off. We don't want to be scary, we want to be nice and fun, and then casually be the best choice around. Real casually.

We believe frameworks should be disposable, and components recyclable. We don't want a web where walled gardens jealously compete with one another. By making the DOM the lowest common denominator, switching from one framework to another becomes frictionless. Choo is modest in its design; we don't believe it will be top of the class forever, so we've made it as easy to toss out as it is to pick up.

We don't believe that bigger is better. Big APIs, large complexities, long files - we see them as omens of impending userland complexity. We want everyone on a team, no matter the size, to fully understand how an application is laid out. And once an application is built, we want it to be small, performant and easy to reason about. All of which makes for easy to debug code, better results and super smiley faces.

Events

At the core of Choo is an event emitter, which is used for both application logic but also to interface with the framework itself. The package we use for this is nanobus.

You can access the emitter through app.use(state, emitter, app), app.route(route, view(state, emit)) or app.emitter. Routes only have access to the emitter.emit method to encourage people to separate business logic from render logic.

The purpose of the emitter is two-fold: it allows wiring up application code together, and splitting it off nicely - but it also allows communicating with the Choo framework itself. All events can be read as constants from state.events. Choo ships with the following events built in:

'DOMContentLoaded'|state.events.DOMCONTENTLOADED

Choo emits this when the DOM is ready. Similar to the DOM's 'DOMContentLoaded' event, except it will be emitted even if the listener is added after the DOM became ready. Uses document-ready under the hood.

'render'|state.events.RENDER

This event should be emitted to re-render the DOM. A common pattern is to update the state object, and then emit the 'render' event straight after. Note that 'render' will only have an effect once the DOMContentLoaded event has been fired.

'navigate'|state.events.NAVIGATE

Choo emits this event whenever routes change. This is triggered by either 'pushState', 'replaceState' or 'popState'.

'pushState'|state.events.PUSHSTATE

This event should be emitted to navigate to a new route. The new route is added to the browser's history stack, and will emit 'navigate' and 'render'. Similar to history.pushState.

'replaceState'|state.events.REPLACESTATE

This event should be emitted to navigate to a new route. The new route replaces the current entry in the browser's history stack, and will emit 'navigate' and 'render'. Similar to history.replaceState.

'popState'|state.events.POPSTATE

This event is emitted when the user hits the 'back' button in their browser. The new route will be a previous entry in the browser's history stack, and immediately afterward the'navigate' and 'render'events will be emitted. Similar to history.popState. (Note that emit('popState') will not cause a popState action - use history.go(-1) for that - this is different from the behaviour of pushState and replaceState!)

'DOMTitleChange'|state.events.DOMTITLECHANGE

This event should be emitted whenever the document.title needs to be updated. It will set both document.title and state.title. This value can be used when server rendering to accurately include a <title> tag in the header. This is derived from the DOMTitleChanged event.

State

Choo comes with a shared state object. This object can be mutated freely, and is passed into the view functions whenever 'render' is emitted. The state object comes with a few properties set.

When initializing the application, window.initialState is used to provision the initial state. This is especially useful when combined with server rendering. See server rendering for more details.

state.events

A mapping of Choo's built in events. It's recommended to extend this object with your application's events. By defining your event names once and setting them on state.events, it reduces the chance of typos, generally autocompletes better, makes refactoring easier and compresses better.

state.params

The current params taken from the route. E.g. /foo/:bar becomes available as state.params.bar If a wildcard route is used (/foo/*) it's available as state.params.wildcard.

state.query

An object containing the current queryString. /foo?bin=baz becomes { bin: 'baz' }.

state.href

An object containing the current href. /foo?bin=baz becomes /foo.

state.route

The current name of the route used in the router (e.g. /foo/:bar).

state.title

The current page title. Can be set using the DOMTitleChange event.

state.components

An object recommended to use for local component state.

state.cache(Component, id, [...args])

Generic class cache. Will lookup Component instance by id and create one if not found. Useful for working with stateful components.

Routing

Choo is an application level framework. This means that it takes care of everything related to routing and pathnames for you.

Params

Params can be registered by prepending the route name with :routename, e.g. /foo/:bar/:baz. The value of the param will be saved on state.params (e.g. state.params.bar). Wildcard routes can be registered with *, e.g. /foo/*. The value of the wildcard will be saved under state.params.wildcard.

Default routes

Sometimes a route doesn't match, and you want to display a page to handle it. You can do this by declaring app.route('*', handler) to handle all routes that didn't match anything else.

Querystrings

Querystrings (e.g. ?foo=bar) are ignored when matching routes. An object containing the key-value mappings exists as state.query.

Hash routing

By default, hashes are ignored when routing. When enabling hash routing (choo({ hash: true })) hashes will be treated as part of the url, converting /foo#bar to /foo/bar. This is useful if the application is not mounted at the website root. Unless hash routing is enabled, if a hash is found we check if there's an anchor on the same page, and will scroll the element into view. Using both hashes in URLs and anchor links on the page is generally not recommended.

Following links

By default all clicks on <a> tags are handled by the router through the nanohref module. This can be disabled application-wide by passing { href: false } to the application constructor. The event is not handled under the following conditions:

  • the click event had .preventDefault() called on it
  • the link has a target="_blank" attribute with rel="noopener noreferrer"
  • a modifier key is enabled (e.g. ctrl, alt, shift or meta)
  • the link's href starts with protocol handler such as mailto: or dat:
  • the link points to a different host
  • the link has a download attribute

:warn: Note that we only handle target=_blank if they also have rel="noopener noreferrer" on them. This is needed to properly sandbox web pages.

Navigating programmatically

To navigate routes you can emit 'pushState', 'popState' or 'replaceState'. See #events for more details about these events.

Server Rendering

Choo was built with Node in mind. To render on the server call .toString(route, [state]) on your choo instance.

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

var app = choo()
app.route('/', function (state, emit) {
  return html`<div>Hello ${state.name}</div>`
})

var state = { name: 'Node' }
var string = app.toString('/', state)

console.log(string)
// => '<div>Hello Node</div>'

When starting an application in the browser, it's recommended to provide the same state object available as window.initialState. When the application is started, it'll be used to initialize the application state. The process of server rendering, and providing an initial state on the client to create the exact same document is also known as "rehydration".

For security purposes, after window.initialState is used it is deleted from the window object.

<html>
  <head>
    <script>window.initialState = { initial: 'state' }</script>
  </head>
  <body>
  </body>
</html>

Components

From time to time there will arise a need to have an element in an application hold a self-contained state or to not rerender when the application does. This is common when using 3rd party libraries to e.g. display an interactive map or a graph and you rely on this 3rd party library to handle modifications to the DOM. Components come baked in to Choo for these kinds of situations. See nanocomponent for documentation on the component class.

// map.js
var html = require('choo/html')
var mapboxgl = require('mapbox-gl')
var Component = require('choo/component')

module.exports = class Map extends Component {
  constructor (id, state, emit) {
    super(id)
    this.local = state.components[id] = {}
  }

  load (element) {
    this.map = new mapboxgl.Map({
      container: element,
      center: this.local.center
    })
  }

  update (center) {
    if (center.join() !== this.local.center.join()) {
      this.map.setCenter(center)
    }
    return false
  }

  createElement (center) {
    this.local.center = center
    return html`<div></div>`
  }
}
// index.js
var choo = require('choo')
var html = require('choo/html')
var Map = require('./map.js')

var app = choo()
app.route('/', mainView)
app.mount('body')

function mainView (state, emit) {
  return html`
    <body>
      <button onclick=${onclick}>Where am i?</button>
      ${state.cache(Map, 'my-map').render(state.center)}
    </body>
  `

  function onclick () {
    emit('locate')
  }
}

app.use(function (state, emitter) {
  state.center = [18.0704503, 59.3244897]
  emitter.on('locate', function () {
    window.navigator.geolocation.getCurrentPosition(function (position) {
      state.center = [position.coords.longitude, position.coords.latitude]
      emitter.emit('render')
    })
  })
})

Caching components

When working with stateful components, one will need to keep track of component instances โ€“ state.cache does just that. The component cache is a function which takes a component class and a unique id (string) as its first two arguments. Any following arguments will be forwarded to the component constructor together with state and emit.

The default class cache is an LRU cache (using nanolru), meaning it will only hold on to a fixed amount of class instances (100 by default) before starting to evict the least-recently-used instances. This behavior can be overriden with options.

Optimizations

Choo is reasonably fast out of the box. But sometimes you might hit a scenario where a particular part of the UI slows down the application, and you want to speed it up. Here are some optimizations that are possible.

Caching DOM elements

Sometimes we want to tell the algorithm to not evaluate certain nodes (and its children). This can be because we're sure they haven't changed, or perhaps because another piece of code is managing that part of the DOM tree. To achieve this nanomorph evaluates the .isSameNode() method on nodes to determine if they should be updated or not.

var el = html`<div>node</div>`

// tell nanomorph to not compare the DOM tree if they're both divs
el.isSameNode = function (target) {
  return (target && target.nodeName && target.nodeName === 'DIV')
}

Reordering lists

It's common to work with lists of elements on the DOM. Adding, removing or reordering elements in a list can be rather expensive. To optimize this you can add an id attribute to a DOM node. When reordering nodes it will compare nodes with the same ID against each other, resulting in far fewer re-renders. This is especially potent when coupled with DOM node caching.

var el = html`
  <section>
    <div id="first">hello</div>
    <div id="second">world</div>
  </section>
`

Pruning dependencies

We use the require('assert') module from Node core to provide helpful error messages in development. In production you probably want to strip this using unassertify.

To convert inlined HTML to valid DOM nodes we use require('nanohtml'). This has overhead during runtime, so for production environments we should unwrap this using the nanohtml transform.

Setting up browserify transforms can sometimes be a bit of hassle; to make this more convenient we recommend using bankai build to build your assets for production.

FAQ

Why is it called Choo?

Because I thought it sounded cute. All these programs talk about being "performant", "rigid", "robust" - I like programming to be light, fun and non-scary. Choo embraces that.

Also imagine telling some business people you chose to rewrite something critical for serious bizcorp using a train themed framework. ๐Ÿš‚ ๐Ÿš‹ ๐Ÿš‹ ๐Ÿš‹

Is it called Choo, Choo.js or...?

It's called "Choo", though we're fine if you call it "Choo-choo" or "Chugga-chugga-choo-choo" too. The only time "choo.js" is tolerated is if / when you shimmy like you're a locomotive.

Does Choo use a virtual-dom?

Choo uses nanomorph, which diffs real DOM nodes instead of virtual nodes. It turns out that browsers are actually ridiculously good at dealing with DOM nodes, and it has the added benefit of working with any library that produces valid DOM nodes. So to put a long answer short: we're using something even better.

How can I support older browsers?

Template strings aren't supported in all browsers, and parsing them creates significant overhead. To optimize we recommend running browserify with nanohtml as a global transform or using bankai directly.

$ browserify -g nanohtml

Is choo production ready?

Sure.

API

This section provides documentation on how each function in Choo works. It's intended to be a technical reference. If you're interested in learning choo for the first time, consider reading through the handbook first โœจ

app = choo([opts])

Initialize a new choo instance. opts can also contain the following values:

  • opts.history: default: true. Listen for url changes through the history API.
  • opts.href: default: true. Handle all relative <a href="<location>"></a> clicks and call emit('render')
  • opts.cache: default: undefined. Override default class cache used by state.cache. Can be a a number (maximum number of instances in cache, default 100) or an object with a nanolru-compatible API.
  • opts.hash: default: false. Treat hashes in URLs as part of the pathname, transforming /foo#bar to /foo/bar. This is useful if the application is not mounted at the website root.

app.use(callback(state, emitter, app))

Call a function and pass it a state, emitter and app. emitter is an instance of nanobus. You can listen to messages by calling emitter.on() and emit messages by calling emitter.emit(). app is the same Choo instance. Callbacks passed to app.use() are commonly referred to as 'stores'.

If the callback has a .storeName property on it, it will be used to identify the callback during tracing.

See #events for an overview of all events.

app.route(routeName, handler(state, emit))

Register a route on the router. The handler function is passed app.state and app.emitter.emit as arguments. Uses nanorouter under the hood.

See #routing for an overview of how to use routing efficiently.

app.mount(selector)

Start the application and mount it on the given querySelector, the given selector can be a String or a DOM element.

In the browser, this will replace the selector provided with the tree returned from app.start(). If you want to add the app as a child to an element, use app.start() to obtain the tree and manually append it.

On the server, this will save the selector on the app instance. When doing server side rendering, you can then check the app.selector property to see where the render result should be inserted.

Returns this, so you can easily export the application for server side rendering:

module.exports = app.mount('body')

tree = app.start()

Start the application. Returns a tree of DOM nodes that can be mounted using document.body.appendChild().

app.toString(location, [state])

Render the application to a string. Useful for rendering on the server.

choo/html

Create DOM nodes from template string literals. Exposes nanohtml. Can be optimized using nanohtml.

choo/html/raw

Exposes nanohtml/raw helper for rendering raw HTML content.

Installation

$ npm install choo

See Also

  • bankai - streaming asset compiler
  • stack.gl - open software ecosystem for WebGL
  • yo-yo - tiny library for modular UI
  • tachyons - functional CSS for humans
  • sheetify - modular CSS bundler for browserify

Support

Creating a quality framework takes a lot of time. Unlike others frameworks, Choo is completely independently funded. We fight for our users. This does mean however that we also have to spend time working contracts to pay the bills. This is where you can help: by chipping in you can ensure more time is spent improving Choo rather than dealing with distractions.

Sponsors

Become a sponsor and help ensure the development of independent quality software. You can help us keep the lights on, bellies full and work days sharp and focused on improving the state of the web. Become a sponsor

Backers

Become a backer, and buy us a coffee (or perhaps lunch?) every month or so. Become a backer

License

MIT

Comments
  • Discussion: choo/component

    Discussion: choo/component

    update (03/11/17)

    The initial design in this post isn't great; here's a revised version with support for async loading.

    • https://github.com/choojs/choo/issues/593#issuecomment-341785819

    components

    When building out applications, components are a great way to slice UI and its corresponding logic into crisp slices.

    We've created nanocomponent as a solution to allow for encapsulation with DOM diffing. Unforunately it seems to be a tad verbose, which means people have gone around and built wrappers to make things a little simpler.

    These wrappers seem to be mostly focused on two things:

    1. Manage component instances.
    2. Remove boilerplate involved with create a component.

    Challenges

    • Because of the modular nature of components, we can't do the centralized tangly thing that other frameworks can do. Unfortunately :(.
    • We also took it upon ourselves to use "The Web Platform". This means we're limited by odd behavior from the DOM. Not great, but it's a tradeoff we've taken.
    • Most of what we do is about explicit behavior โ€” explicit re-renders, explicit updates, explicit mutations. Being overly explicit can result in boilerplate, and repetition โ€” the art is to find balance.

    Example

    A basic component looks kinda like this:

    var component = require('choo/component')
    var html = require('choo/html')
    
    module.exports = class Button extends component {
      createElement () {
        return html`
          <button>
            Click me
          </button>
        `
      }
    
      update () {
        return false
      }
    }
    

    But that's not super debuggable. Ideally we would allow devtools to inspect all of the state, events, and timings.

    var component = require('choo/component')
    var html = require('choo/html')
    
    module.exports = class Button extends component {
      constructor (name, state, emit) {
        this.state = state
        this.emit = emit
        super(name)
      }
    
      createElement () {
        return html`
          <button onclick=${this._onclick}>
            Click me
          </button>
        `
      }
    
      update () {
        return false
      }
    
      _onclick (e) {
        this.emit('increment', 1)
      }
    }
    

    Ok cool, now we can emit events on the global emitter. When the component is constructed it just needs to be passed the elements. This can be done with something like shared-component. It requires a bit of boilerplate โ€” the exact implementation is not the point.

    Instead I was thinking it might be neat if we could do component registration in a central spot โ€” as a new method on an instance of Choo.

    var choo = require('choo')
    
    var app = choo()
    app.component('button', require('./components/button'))
    app.route('/', require('./views/main'))
    app.mount('body')
    

    Yay, so now we have component registration going on. Folks can proceed to slice up state in whatever way they want. To render components, I was thinking we'd add another method to views so components become accessible.

    var html = require('choo/html')
    
    module.exports = function view (state, emit, components) {
      return html`
        <body>
          ${components.button.render()}
        </body>
      `
    }
    

    components here is an object, similar in behavior to shared-component. I considered stealing some of component-box's behavior, and abstract away the .render() method โ€” but when a component requires another component, it wouldn't have any of those abstractions, and we'd introduce two different APIs to do the same thing. I don't think that might be a good idea. Or maybe we should do it? Argh; this is a tricky decision either way (e.g. consistency + learning curve vs ease of use, half the time).

    Oh, worth noting that because components can now be registered with Choo, we should probably extend Nanocomponent to not require the whole boilerplate:

    var component = require('choo/component')
    var html = require('choo/html')
    
    module.exports = class Button extends component {
      createElement () {
        return html`
          <button onclick=${this._onclick}>
            Click me
          </button>
        `
      }
    
      update () {
        return false
      }
    
      _onclick (e) {
        this.emit('increment', 1)
      }
    }
    

    Ok, that was cool. Exciting!

    Yeah, I know โ€” I'm pretty stoked about this too. Now it is worth pointing out that there's things we're not doing. Probably the main thing we're not doing is managing lists of components. Most of this exists to smooth the gap between application-level components, and the views itself. Our goal has always been to allow people to have a good time building big app โ€” whether it's getting started, debugging or tuning performance.

    Now there's some stuff we'll have to do:

    • We're showing the class keyword everywhere. I think it's useful when building applications because it's way less typing than alternatives. Bankai would need to support it.
    • We'd need to teach people how to use (vector) clocks to keep track of updates. I don't like shallow compares, and just so much boo โ€” let's have some proper docs on the matter.
    • We need to think about our file size marketing. You can totes write a whole app without any components, and like we should promote it โ€” it's heaps easy. But with components, certain patterns become kinda nice too. Is it part of choo core? Wellll, kinda โ€” not core core โ€” but core still.

    What doesn't this do?

    Welllllll, we're not doing the thing where you can keep lots of instances of the same component around. I think dedicated modules for that make sense โ€” Bret's component array thing is probably a good start, and work for more specialized cases from there. For example having an infinite scroller would be neat.

    When will any of this land?

    Errrrr, hold on there โ€” it's just a proposal for now. I'm keen to hear what y'all think! Does this work? Where doesn't this work? Do you have any ideas of how we should do a better job at this? Please share your thoughts! :D

    Summary

    Wellllllp, sorry for being super incoherent in this post; I hope it sorta makes sense. The Tl;Dr is: add a new method for Choo called .component, which looks like this:

    index.js

    var choo = require('choo')
    
    var app = choo()
    app.component('button', require('./components/button'))
    app.route('/', require('./views/main'))
    app.mount('body')
    

    components/button.js

    var component = require('choo/component')
    var html = require('choo/html')
    
    module.exports = class Button extends component {
      createElement () {
        return html`
          <button onclick=${this._onclick}>
            Click me
          </button>
        `
      }
    
      update () {
        return false
      }
    
      _onclick (e) {
        this.emit('increment', 1)
      }
    }
    

    views/main.js

    var html = require('choo/html')
    
    module.exports = function view (state, emit, components) {
      return html`
        <body>
          ${components.button.render()}
        </body>
      `
    }
    

    cc/ @bcomnes @jongacnik @tornqvist

    opened by yoshuawuyts 76
  • [discussion] how can we improve state management?

    [discussion] how can we improve state management?

    As I'm working on a real-world application I'm noticing there's friction using stores, and I'm becoming unsure if namspacing is the best construct available.

    Nested data structures are hard to model and use. Say we're building a giant form website with multiple forms on it, we might have the fields of "form", "questions" and "fields". We might perhaps want to track the state of these separately; as having a single blob make up 90% of our application doesn't quite feel right. "form", "questions" and "fields" all have their own little bits of logic and what not.

    rdbms

    An approach we could do here is have each of these be expressed in a relation of "one-to-one", "many-to-many", "many-to-one" (oneToOne, many, fk in RDBMS-speak respectively). This would allow us to define the fields between the respective items. And flatten them in our representation, making it easier to view and reason about. There's prior art in redux-orm and querying a redux store

    The redux docs state the following:

    In a more complex app, youโ€™re going to want different entities to reference each other. We suggest that you keep your state as normalized as possible, without any nesting. Keep every entity in an object stored with an ID as a key, and use IDs to reference it from other entities, or lists. Think of the appโ€™s state as a database.

    There's also reselect which creates higher level views on top of normalized data, and updates to match. I like this idea because it matches the idea of how I think about data.

    namespaces

    But all of this raises the question: how useful are namespaces at this point? If our data is completely intertwined, and method calls on values rely on all data being available (e.g. "how many fields are completed on this form?"), then what's the point of having namespaces? In a sense we'd be implementing namespaces as a way to interact with our flat data.

    Some considerations we might need to make regarding namespaces:

    • is it even possible to move namespacing to userland?
    • how would namespaced actions work? - could we override this using the new wrappers API from 3.3.0?
    • what do we lose if we move namespacing to userland?
    • what do we gain if we move namespacing to userland?
    • how would it affect our codebase?

    I suspect it's definitely possible, and would shrink the codebase significantly

    • but we should check our facts first tho. We could totally test userland namespace packages on top of existing choo if we just don't namespace our models. Probably it would need to use a different character internally to namespace when sending actions, but that's about it (e.g. don't use : as it's special right now).

    graph

    I feel the endgame of RDBMS and selectors is to implement a graph structure where relationships can be used to query data (e.g. "give me all questions of form foo", "give me all fields of question bar").

    Now what I wonder is if we could leapfrog over the implementation details, of RDBMS and selectors and implement a graph structure directly on top of a single object. This would be similar in spirit to @mcollina's levelgraph.

    Now my knowledge on databases isn't great, and I don't have any prior experience implementing databases (relational, graph or otherwise) so any input here would be grand.

    edit: for those unfamiliar with graph databases, I've found a great article explaining the differences between graph and RDBMS databases: https://neo4j.com/developer/graph-db-vs-rdbms/. Especially the chart showing the same data expressed in the different paradigms is interesting I found:

    relational

    relational architecture example

    graph

    graph architecture example

    wrapping up

    I'd be keen to hear people's thoughts on this. Like I said this post is coming from seeing choo's existing abstractions break down at a larger scale, and the desire to do better. Thanks for making it this far down the post; let me know what you think :v:

    Related issues

    • https://github.com/yoshuawuyts/choo/issues/23

    See Also

    • https://medium.com/@adamrackis/querying-a-redux-store-37db8c7f3b0f#.uaze28qmm
    • https://github.com/reactjs/reselect
    • http://redux.js.org/docs/recipes/ComputingDerivedData.html
    • https://github.com/tommikaikkonen/redux-orm
    • https://github.com/mcollina/levelgraph
    • https://neo4j.com/developer/graph-db-vs-rdbms/
    opened by yoshuawuyts 31
  • improve perf

    improve perf

    Hi,

    I'm testing choo against others and really wish it perform great but it turn out to be really slow there. so I think I should ask you first to ensure that I didn't miss something which mess the results.

    todomvc-pref-choo

    here's the code https://github.com/rabbots/todomvc-perf

    Thanks

    opened by katopz 29
  • Make state immutable in reducers/effects

    Make state immutable in reducers/effects

    Call Object.freeze on state when passing it into to the reducers or effects so that state cannot be modified except when being returned from an emitter.

    Add tests to prove state is immutable in these scenarios.

    opened by toddself 24
  • Questions on the proper use and handling of `done` callbacks

    Questions on the proper use and handling of `done` callbacks

    Preface: trying to use choo on a project, and I'm somewhat confused, on several levels, about the proper usage of the done callback in effects and how it relates to the callback provided in the send method.

    Here are my particular questions and areas where I'd appreciate some clarifications:

    1. In an effect handler, I understand the need to pass the done cb to subsequent send calls, to maintain the chain and allow errors to propagate upwards. But how should I handle the cb if I want to send 2+ actions from a single effect (e.g. I make an API call which returns data applicable to two separate models)? The following seems incorrect, but I'm not sure what the correct behavior should be:
    myeffect: (state, data, send, done) => {
        ... make call to server ...
        send('foo:update', newFoo, done);
        send('bar:update', newBar, done);
    }
    
    1. Aside from effects sending actions to reducers, we often send actions from views. Presumably the send provided to views is the same send provided to effects. So that implies that one could provide their own callback to be called once the sent action has completed. My question is if there's is any common/encouraged use for this? I could imagine using it to update the html/UI if an error was encountered (which seems generally useful). But I could also imagine that such usage could present a race with the view being retriggered (because the state changed) and all sorts of unpredictable/undesired behavior occurring...

    2. Are there any examples of applications making use of the done callback in a non-trivial manner? It would be great to have something to reference to, and none of the examples in this repository seem to make use of it (there are cases of passing it an error, but as far as I can tell nothing acts on that error).

    Thanks!

    opened by oderby 22
  • How to avoid rerendering a certain component?

    How to avoid rerendering a certain component?

    I have a page on which a lot of widgets. All widgets frequently polls the server. And one of these widgets is the timer that simply shows the time and it has no any business logic. Timer ticks every second and every second WHOLE view is rerendered! But I want to make this a timer ticking, and only changed his view. Is there any do it or I in all cases is to use the state?

    opened by kwoon 19
  • discussion: Roadmap

    discussion: Roadmap

    Sharing a bit of a higher level overview currently only in my brain so all y'all know what's going on:

    3.0 Use CPS

    • get nested effects and error / value propagation
    • streamline view api
    • trim bytes by making yo-yoify applicable

    3.1 Performance

    • add request animation frame to cap updates and better handle burst updates
    • provide standalone bundles for inclusion in codepen and the like

    3.2 Plugins

    • app.use() API (oh god)

    3.2.1 fix path triggers

    • fix the send(location:href) call

    3.3 Custom async paradigms

    • provide wrap hooks so alternative async implementations become first-class citizens (e.g. choo-pull, choo-xstream, choo-rxjs, etc.)

    4.0 Routing

    • upgrade sheet-router to handle hash routes by default; would make handling routes a complete no-brainer (it can be a bit tricky rn when using both hashes and normal routes)
    • update sheet-router to no longer use route()

    4.1 Components

    This is where we add one of the last large feature additions: choo/component.

    • add choo/component based on nanocomponent
    • replace const with var, all the way down

    5.0 polish mode

    I think at this point choo is quite stable. The syntax will probably not change; we'll just keep making things smaller and faster.

    • switch over to nanomorph
    • allow querystrings to become part of routes
    • remove on-load from choo/html now that it's part of choo/component
    • remove prev from elements as nobody uses it anyway
    • change stack trace callback to keep history inside hooks

    5.1 / 6.0 threads

    • use threaded effect and reducer execution using webworkers

    Goals not (yet) bound to roadmap

    • [ ] look into trimming all .forEach() and .map() calls and replacing them with ES3 equivalents - speed and less array allocations ftw
    • [ ] get (dev)tools out there that can visualize all action calls and lay them out visually - would make debugging silky smooth
    • [ ] add a whole bunch of benchmarks so we know how choo is doing compared to itself
    opened by yoshuawuyts 19
  • update view API to be (state, prev, send)

    update view API to be (state, prev, send)

    Since v2.1.4 (oops, semver) we're now also passing the oldState down to views. The view api now looks like:

    const view = (params, state, send, oldState) => choo.view`
      <main>${params.foo} and ${state.bar}</main>
    `
    

    Instead I reckon it might be nicer to have:

    const view = (state, oldState, send) => choo.view`
      <main>${state.params.foo} and ${state.bar}</main>
    `
    

    This would definitely be a breaking change, but actually not that hard to fix (e.g. we can provide a regex heyyyy). What do people think?

    opened by yoshuawuyts 18
  • Devtools for choo

    Devtools for choo

    Hey, thanks for the great little framework! I would like to take a stab at implementing a browser devtool for choo.

    In the only previous suggestion for this feature ( #152 ) the suggested API would use the plugins for that:

    const choo = require('choo')
    const devtools = require('choo-devtools')
    
    const app = choo()
    app.use(devtools())
    

    Does that still reflect the current thinking on the issue?

    opened by michalczaplinski 17
  • choo Yeoman generator

    choo Yeoman generator

    I'm gathering interest in a Yeoman generator for choo and asking for help if needed.

    The point of this generator would be to be the simplest possible configโ€”no 20gb of dependencies like with other libraries and frameworks. Just enough to get started.

    My wish list:

    • Dev build (budo?)
    • Production build (minify + concat)
    • Some sort of simple CSS/styling solution
    • Very mininal example (any of the projects in the examples directory will do)

    Would be awesome for me:

    • One step deployment with Surge or Now (run build step and then run deployment script)

    I'll stick with Browserify as that seems to be preferred and Webpack configs are ugly ๐Ÿš‚ I'll take care of most of the code, just need opinions/help on the build steps.

    Let me know what you think!

    opened by sotojuan 17
  • Choo v5 / v6 onload handlers on root node not called when navigating

    Choo v5 / v6 onload handlers on root node not called when navigating

    Steps to reproduce behavior

    Create two pages:

    • /foo with root <div>
    • /bar with root <div onload=${myHandler}>

    Expected behavior

    Navigating from /foo to /bar triggers the onload handler

    Actual behavior

    The onload handler is not triggered upon navigation. It is triggered when loading the /bar page initially though.

    Workaround

    Attaching the onload handler to a child node fixes the issue.

    The workaround is simple enough to apply. If this is tricky to solve, I think an assertion would also do.

    opened by mantoni 16
  • Wrong return type for `mount`

    Wrong return type for `mount`

    Expected behavior

    The return type should be something | undefined where something is I guess a Choo instance? Or something like that.

    Actual behavior

    It is void.

    opened by fgblomqvist 0
  • Usage with http-server

    Usage with http-server

    Expected behavior

    from my package.json

    "start": "http-server -o -c-1 -p 8080 --proxy http://localhost:8080/ ",
    

    This indeed loads up the app properly at http://127.0.0.1:8080/ However

      function joinView(state, emit) {
        return html`<body>
          <div><h4>Join!</h4></div>
        </body>`;
      }
      function lobby(state, emit) {
        return html`<body>
          <div>
            <h4>Lobby</h4>
            <a href="/join">Join</a>
          </div>
        </body>`;
      }
      app.use(devtools());
      app.use((state) => {
        state.logger = false;
      });
      app.route("/join", joinView);
      app.route("/", lobby);
      app.mount("body");
    

    If i click the link "Join" from within the app, the router properly pushStates to /join and renders the joinView but if I reload so that the browser tries to request /join... i run into a loop because I'm trying to force http-server to do a catchall and serve my index.html which just has a simple div.

    <html>
      <head>
        <script src="bundle.js"></script>
      </head>
      <body></body>
    </html>
    
    

    What precisely should I do to handle navigating directly to /join? Without the proxy statement /join doesn't exist because its not really something available outside the SPA. @YerkoPalma

    opened by rook2pawn 1
  • Route handler return value

    Route handler return value

    Where in the source code does choo handle the return value of route handler functions (the hyperx html)? I see where where the router sets the handler, but I don't see where choo takes that return value and renders it.

    opened by NHQ 3
  • Experimenting with custom elements

    Experimenting with custom elements

    Hi there ๐Ÿ‘‹

    I'm using Choo for a large app and I'm currently exploring how I can put custom elements to use. I have some nicely working hacks that I've put into a repo to play around with (tested with Safari and Chrome):

    https://github.com/mantoni/choo-custom-elements

    Some thoughts / questions:

    • Can we make it somewhat less hacky (e.g. the onrender setter)?
    • Is there an interest in finding an "official" way to do this? ๐Ÿ‘ฎโ˜๏ธ๐Ÿ˜„
    • Would it make sense to create a base class or a choo extension to make this easy?
    opened by mantoni 0
  • New feature: Async route support

    New feature: Async route support

    Hi, Really liking Choo so far! One thing I was missing was the ability to define an async route (I'm working on a game and wanted to preload assets). I didn't see a simple way to accomplish this (maybe I missed something obvious), so I wrote a little module that augments app with an asyncRoute method:

    const app = withAsyncRoute(choo());
    
    app.asyncRoute('/', loadResources,
        () => html`<div id="root"><h1>Loading...</h1></div>`, 
        (state, emit, data) => html`<div id="root">Loaded ${data}</div>`
        (state, emit, err, params) => {
            console.error('error loading /:', params, err, state);
            return html`
                <div id="root">
                    <h1 style="color: red">Error loading / (see console)</h1>
                </div>
            `;
        },
    );
    

    Was wondering if there's interest in either merging this function into Choo proper, or as a third-party npm module?

    For reference, here's the module -- it's pretty simple:

    module.exports = (app) => Object.assign(app, {
        /**
         * @param  {string} route
         * @param  {Promise<Data>|Function<Promise<Data>>}       promise
         * @param  {Function<State, Emitter, RouteParams>}       loadingHandler
         * @param  {Function<State, Emitter, Data, RouteParams>} loadedHandler
         * @param  {Function<State, Emitter, *, RouteParams>}    errorHandler
         */
        asyncRoute: (route, promise, loadingHandler, loadedHandler, errorHandler) => {
            app.use((state, emitter) => {
                const emit = emitter.emit.bind(emitter);
    
                app.router.on(route, (params) => {
                    state.params = params;
    
                    if (typeof promise === 'function') {
                        promise = promise();
                    }
    
                    let completed = false;
                    let isError = false;
                    let data = null;
    
                    promise.then(result => {
                        completed = true;
                        data = result;
                        emitter.emit('render');
                    }).catch(err => {
                        completed = isError = true;
                        data = err;
                        emitter.emit('render');
                    });
    
                    return () => {
                        if (!completed) {
                            return loadingHandler(state, emit, params);
                        } else if (isError) {
                            return errorHandler(state, emit, data, params);
                        } else {
                            return loadedHandler(state, emit, data, params);
                        }
                    };
                });
            });
        }
    });
    
    opened by vgel 1
  • Can I pass a rootNode to `nanohref`?

    Can I pass a rootNode to `nanohref`?

    I am trying to mount a choo application in an "application shell" that is provided by a static site generator.

    I would expect the routing behavior to be limited to the scope of choo's application host, yet nanohref will try to handle all anchors being clicked.

    Looking at the nanohref docs, this behavior seems to be supported by passing a second rootNode argument to the nanohref function: https://github.com/choojs/nanohref#nanohrefhandlerlocation-rootnode

    Looking at how choo itself is using nanohref it looks like passing a second argument is not intended: https://github.com/choojs/nanohref#nanohrefhandlerlocation-rootnode

    This leads me to two questions:

    1. Is it possible to achieve this behavior in some other way?
    2. In case no, would you accept a PR that enables passing the application root to nanohref as well? In this case I have to admit I am slightly unsure how the API for this could even look like considering how there is basically zero surface area for options in choo.
    opened by m90 5
Releases(v7.0.0-0)
  • v7.0.0-0(Jun 12, 2019)

    Install

    $ npm install choo@next
    

    Hash routing has been disabled by default (https://github.com/choojs/choo/pull/699)

    Hashes are by default ignored when routing. To enable hash routing, set the hash option to true choo({ hash: true }).

    Match route before intializing stores (https://github.com/choojs/choo/pull/698)

    This means that href, query, param and route are exposed on state before stores are initialized. This can be usefull while setting up the initial state.

    State is not mutated during server side render (https://github.com/choojs/choo/pull/649)

    Prior to this change, when calling toString, changes to state would persist in-between renders. This could lead to a polluted state. This change also clears all event listeners before initializing stores, meaning your listeners are ensured to only trigger once, even on consecutive calls of toString.

    If you were doing server side rendering prior to this update, you were probably plucking at the application state after rendering to get at the generated state. You can now be assured that it is only the state that is passed in to toString that is mutated and can e.g. be used to expose initialState to the client.

    var html = app.toString('/', state)
    - var title = app.state.title
    + var title = state.title
    

    Use Object.assign instead of xtend (https://github.com/choojs/choo/pull/616)

    This change has an effect on browser support. By removing the dependency xtend, bundle size is reduced but support for IE11 is dropped. We recommend the polyfill service https://polyfill.io which will detect legacy browsers and load the appropiate polyfills.

    Note: The dependency xtend has been dropped in all other tools in the choo universe, i.e. choo-devtools and choo-service-worker etc.

    Source code(tar.gz)
    Source code(zip)
  • v6.13.3(Apr 23, 2019)

  • v6.13.2(Apr 2, 2019)

  • v6.13.1(Oct 30, 2018)

    • Remove references to bel from documentation (@tornqvist in #678)
    • Add v6 tests (@timwis in #674)
    • Fix inspect npm script (@goto-bus-stop in #654)
    • Some spelling & typo fixes in readme (@dbtek in #688)
    • Fix wrong this usage in nanohref integration (@mantoni in #689)
    Source code(tar.gz)
    Source code(zip)
  • v6.13.0(Jul 12, 2018)

    Changes

    • Added documentation on components
    • Added hash option for disabling hash routing

    Related Pull Requests

    • https://github.com/choojs/choo/pull/673
    • https://github.com/choojs/choo/pull/667
    Source code(tar.gz)
    Source code(zip)
  • v6.12.1(Jun 13, 2018)

  • v6.12.0(Jun 13, 2018)

    The :100:th release tag!

    • Add UMD build (@heyitsmeuralex in #617)
    • Update bel and yo-yoify references in docs to nanohtml (@goto-bus-stop in #660)
    • ci: Test on Node 10 (@goto-bus-stop in #661)
    • Update dependencies (@YerkoPalma in #663)
    • typings: Add app argument to app.use callback (@seangenabe in #665)
    Source code(tar.gz)
    Source code(zip)
  • v6.8.0(Feb 14, 2018)

    Thanks everyone that helped on this release!

    Changes

    • Fixed documentation for the popState event
    • Updates nanotiming to v7, fixing issues with many timings in Safari 11
    • Allows using app.mount(selector) when server rendering, exposing the target selector as app.selector for templating

    Related Pull Requests

    • https://github.com/choojs/choo/pull/626
    • https://github.com/choojs/choo/pull/628
    • https://github.com/choojs/choo/pull/625
    Source code(tar.gz)
    Source code(zip)
  • v6.7.0(Jan 17, 2018)

    We upgraded our router to fix some inconsistencies in routing, courtesy of @marcbachmann! :tada:

    Thanks everyone that helped on this release!

    Changes

    • Fixes the routing to modify the state before the navigate event happens. This means you can access the updated state within the navigate event. Modifies the state before the render event is triggered. Therefore this fixes #530, #553, #549, #610, #463
    • Exposes .emit as an alias to .emitter.emit

    Related Pull Requests

    • https://github.com/choojs/choo/pull/613
    Source code(tar.gz)
    Source code(zip)
  • 6.1.0(Sep 20, 2017)

Owner
choo
(very) small browser tools. Choo choo
choo
Dojo Framework. A Progressive Framework for Modern Web Apps

@dojo/framework Dojo is a progressive framework for modern web applications built with TypeScript. Visit us at dojo.io for documentation, tutorials, c

Dojo 549 Dec 25, 2022
Frontend compiler, inspired by Svelte

Malina.js Malina.js builds your web-application to use it without framework on frontend side. Therefore your web-app becomes thinner and faster, and t

Malina.js 1.1k Dec 29, 2022
๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

Supporting Vue.js Vue.js is an MIT-licensed open source project with its ongoing development made possible entirely by the support of these awesome ba

vuejs 201.6k Jan 7, 2023
One framework. Mobile & desktop.

Angular - One framework. Mobile & desktop. Angular is a development platform for building mobile and desktop web applications using Typescript/JavaScr

Angular 85.6k Dec 31, 2022
Ember.js - A JavaScript framework for creating ambitious web applications

Ember.js is a JavaScript framework that greatly reduces the time, effort and resources needed to build any web application. It is focused on making yo

Ember.js 22.4k Jan 4, 2023
๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

vue-next This is the repository for Vue 3.0. Quickstart Via CDN: <script src="https://unpkg.com/vue@next"></script> In-browser playground on Codepen S

vuejs 34.6k Jan 4, 2023
The tiny framework for building hypertext applications.

Hyperapp The tiny framework for building hypertext applications. Do more with lessโ€”We have minimized the concepts you need to learn to get stuff done.

Jorge Bucaran 18.9k Jan 1, 2023
๐ŸŒฑ React and redux based, lightweight and elm-style framework. (Inspired by elm and choo)

English | ็ฎ€ไฝ“ไธญๆ–‡ dva Lightweight front-end framework based on redux, redux-saga and react-router. (Inspired by elm and choo) Features Easy to learn, eas

null 16.1k Jan 4, 2023
Relay is a JavaScript framework for building data-driven React applications.

Relay ยท Relay is a JavaScript framework for building data-driven React applications. Declarative: Never again communicate with your data store using a

Facebook 17.5k Jan 1, 2023
A rugged, minimal framework for composing JavaScript behavior in your markup.

Alpine.js Alpine.js offers you the reactive and declarative nature of big frameworks like Vue or React at a much lower cost. You get to keep your DOM,

Alpine.js 22.5k Jan 2, 2023
The AMP web component framework.

AMP โšก โšก โšก โšก Metrics Tooling AMP is a web component framework for easily creating user-first websites, stories, ads, emails and more. AMP is an open so

AMP 14.9k Jan 4, 2023
A JavaScript Framework for Building Brilliant Applications

mithril.js What is Mithril? Installation Documentation Getting Help Contributing What is Mithril? A modern client-side JavaScript framework for buildi

null 13.5k Dec 28, 2022
Front End Cross-Frameworks Framework - ๅ‰็ซฏ่ทจๆก†ๆžถ่ทจๅนณๅฐๆก†ๆžถ

English | ็ฎ€ไฝ“ไธญๆ–‡ Omi - Front End Cross-Frameworks Framework Merge Web Components, JSX, Virtual DOM, Functional style, observe or Proxy into one framewor

Tencent 12.5k Dec 31, 2022
The Aurelia 1 framework entry point, bringing together all the required sub-modules of Aurelia.

aurelia-framework Aurelia is a modern, front-end JavaScript framework for building browser, mobile, and desktop applications. It focuses on aligning c

aurelia 11.7k Jan 7, 2023
A modest JavaScript framework for the HTML you already have

Stimulus A modest JavaScript framework for the HTML you already have Stimulus is a JavaScript framework with modest ambitions. It doesn't seek to take

Hotwire 11.7k Dec 29, 2022
A functional and reactive JavaScript framework for predictable code

Cycle.js A functional and reactive JavaScript framework for predictable code Website | Packages | Contribute | Chat | Support Welcome Question Answer

Cycle.js 10.2k Jan 4, 2023
๐Ÿฐ Rax is a progressive React framework for building universal application. https://rax.js.org

Rax is a progressive React framework for building universal applications. ?? Write Once, Run Anywhere: write one codebase, run with Web, Weex, Node.js

Alibaba 7.8k Dec 31, 2022
CrossUI is a free Cross-Browser Javascript framework with cutting-edge functionality for rich web application

CrossUI is a free Cross-Browser Javascript framework with cutting-edge functionality for rich web application

Jack Li 1.4k Jan 3, 2023
App development framework based on cocos creator3.1.1

todo: Waiting for English translation cx-cocos App development framework based on cocos creator3.1.1 ไธ€ไธชๅŸบไบŽcocos creator3.1.1็š„ๅบ”็”จAppๅ’Œๆธธๆˆๅผ€ๅ‘ๆก†ๆžถ ๅ…ณ้”ฎ่ฏ๏ผšcocos cre

null 63 Dec 7, 2022