Giter Club home page Giter Club logo

choo's Introduction

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. :steam_locomotive::train::train::train:

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 :sparkles:

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

choo's People

Contributors

aknuds1 avatar bendrucker avatar clkao avatar dmotz avatar emilbayes avatar goto-bus-stop avatar graforlock avatar greenkeeperio-bot avatar haroenv avatar izumisy avatar jbergstroem avatar latentflip avatar mantoni avatar marcbachmann avatar mattmcfarland avatar mickaelandrieu avatar moszeed avatar nicknikolov avatar paulsonnentag avatar perguth avatar seangenabe avatar sethvincent avatar tgfjt avatar timwis avatar toddself avatar tornqvist avatar ungoldman avatar yerkopalma avatar yoshuawuyts avatar zigomir avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

choo's Issues

3.0 release log

We're probably going to run some breaking changes, for the sake of a better API. These changes should get us around 5kb optimized.

  • move choo.view out to require('choo/html') #71 | pr #103
  • streamline view API #35 | pr #111
  • higher order functions #34 | pr #104
  • create lifecycle hooks #1 | feature addition in dependency covered by semver
  • implement state hooks #15 | pr #104
  • add yo-yoify #3 | pr #110
  • rename "app" namespace #82 | pr #111
  • enable browser testing | pr #86
  • propagating actions creates infinite loop #114 | pr #104

When everything is merged:

  • validate all tests pass
  • validate all examples work
  • validate all docs are updated
  • write changelog

Please do comment if I've missed anything; I'm not a fan of breaking changes, but if it needs breaking this is the time to speak up. Cheers!

Access an immutable version of state via subscribers

I mentioned this on irc earlier today. On-demand, synchronous access to the data in the state isn't 100% necessary in probably 99% of use-cases, but it does make it easier to work with a choo component from the outside.

Example: if someone wanted to access part of the state object (i.e. the data driving a boxcar view) in order to serialize it somewhere else:

const emitter = require('events')()
const app = choo()
app.model({
  state: {
    data: [],
    moreState: [].
    evenMore: {}
  },
  subscriptions: [
    (send, state) => emitter.on('getData', (cb) => cb(null, state.data)
  ]
})
return emitter

Then someone could do:

boxcar.emit('getData', (err, data) => serializeData(data))

Rendering without app.router()

Application Stuff

First of all, I really appreciate this idea and movement for a slimmer, faster JS; really impressed with the roots that you have laid out here. I have a question (I am not assuming that I am correct here, that's up to you), does a router need to be required? I understand that the goal is to create a really small, powerful framework, but I believe I am experiencing a small issue because of the router. I am implementing <a href="#some-thing"> in my application using electron, and I believe the router may be confusing events when the anchor tag is clicked and the router re-renders the entire page.

export default (params, state, send) => choo.view`
<div class="wrapper">
  ${sidebar(params, state, send)}
  ${simulator(params, state, send)}
</div>
`;
const app = choo();
app.router((route) => [
  route('/', require('./src/components/main'))
]);
document.addEventListener('DOMContentLoaded', () => {
  document.body.appendChild(tree);
});

logging hooks for actions and state

In development I want to hook into choo to log all actions as they happen, and having access to state so I can bind it to window.state - it would make debugging a lot better

Is state immutable in a reducer?

I've been considering state to be immutable within the context of a reducer -- it looks like the state you get in the arguments is generated with xtend, right?

I'm wondering through, what is the best way to provide a new state for an array?

I've been doing something similar to:

const data = state.data.slice(0)
data.splice(data.findIndex(r => r.id == action.id), 1)
return {data: data}

In a reducer, but it feels odd to be creating a clone of the array there, but it feels odd to do:

state.data.splice(state.date.findIndex(r => r.id === action.id), 1)
return {data: state.data}

Effects shouldn't trigger re-render

According to the documentation on effects, Unlike reducers they cannot modify the state. However, because send-action calls the onchange handler every time send() is called, choo re-renders the view, which is of course unnecessary since the state hasn't changed.

Here is a demo that shows the view is re-rendered by the get effect. A second later, it gets re-rendered by the receive effect, where the state actually changed.

One solution here may be to post an issue in the send-action repo that onchange shouldn't be called unless the state's actually changed. But another way to think of it is that choo's effects should not trigger the onchange handler at all. Thoughts?

Where is this send comes from ?

const document = require('global/document')
const choo = require('choo')
const http = require('choo/http')
const app = choo()

function view (params, state, send) {
  return choo.view`
    <form onsubmit=${onSubmit}>
      <fieldset>
        <label>username</label>
        <input type="text" name="username" autofocus>
      </fieldset>
      <fieldset>
        <label>password</label>
        <input type="password" name="password">
      </fieldset>
      <input type="submit" value="Submit">
    </form>
  `

  function onSubmit (event) {
    send('login', { data: new FormData(event.target) }) // where is the send comes from ?
    event.preventDefault()
  }
}

app.model({
  effects: {
    login: (action, state, send) => {
      http.post('/login', { body: action.data }, (err, res, body) => {
        send('authorize', { payload: body })
      })
    }
  }
})

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

app.start()

edit by @yoshuawuyts: updated for syntax highlighting

supporting anchor links

Using the default sheet-router/href module results in not being able to jump to anchor links when clicked or when first loading a page with a hash that's meant to point to an anchor somewhere on the page.

Supporting anchor links might not be something we want to have in choo by default, but it would be nice to have docs for it somewhere.

This is also potentially a topic for sheet-router.

Here's where I'm at currently with a solution that overrides the default href behavior:

var choo = require('choo')
var el = choo.view
var app = choo()

app.model({
  namespace: 'app',
  subscriptions: [catchLinks]
})

function catchLinks (send) {
  window.onclick = (e) => {
    const node = (function traverse (node) {
      if (!node) return
      if (node.localName !== 'a') return traverse(node.parentNode)
      if (node.href === undefined) return traverse(node.parentNode)
      if (window.location.host !== node.host) return traverse(node.parentNode)
      return node
    })(e.target)
    if (!node) return
    e.preventDefault()
    var href = node.href

    if (location.pathname !== node.pathname) {
      send('app:location', { location: href.replace(/#$/, '') })
      window.history.pushState(null, null, href)
    } else {
      if (window.location.hash !== node.hash) {
        window.location.hash = node.hash
      }
      scrollToHash(node.hash)
    }
  }
}

app.router(function (route) {
  return [
    route('/', home),
    route('/example', example)
  ]
})

var tree = app.start({ href: false })
document.body.appendChild(tree)

function home (params, state, send) {
  return el`
    <main>
    <h1>home</h1>
    <a href="#home-anchor">home anchor link</a>

    <p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p>

    <h2 id="home-anchor">home anchor target</h2>
    <a href="/example#example-anchor">link to example anchor</a>
    <p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p>
    </main>
  `
}

function example (params, state, send) {
  return el`
    <main>
    <h1>example page</h1>
    <a href="#example-anchor">example anchor link</a>

    <p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p>

    <h2 id="example-anchor">example anchor target</h2>
    <a href="/#home-anchor">link to home anchor</a>
    <p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p>
    </main>
  `
}

function scrollToHash (hash) {
  if (hash) {
    var el = document.querySelector(hash)
    window.scrollTo(0, el.offsetTop)
  }
}

contentedtiable elements.

const choo = require('choo')
const app = choo()

app.model({
  state: { title: 'Set the title' },
  reducers: {
    update: (action, state) => ({ title: action.value })
  }
})

const mainView = (params, state, send) => choo.view`
  <main>
    <h1>${state.title}</h1>
    <div
      contenteditable="true"
      oninput=${(e) => send('update', { value: e.target.value })}>Type here</div>
  </main>
`

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

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

Doesn't update the state, missing something?

improve demos

  • add a lil README of what the demo does and how to run it
  • publish to github pages

Programmatic navigation doesn't change URL

I have a form that, when submitted, saves the contents to the state (via send) and redirects you to another page. The documentation on [router] explains how to programmatically navigate the page:

If you want to modify the location programmatically the reducer for the location can be called using send('app:location', { location: href }).

Doing this doesn't change the URL, however, so if the user reloads or bookmarks the page, they won't be taken to the right place. Would you recommend adding a line such as:

window.history.pushState(null, null, '/foo')

Or is there a way to accomplish that with a single line?

web worker example

would be cool to have an example using web workers as an effect:

  • write web worker example
  • add docs (under effects)

cc/ @orodio - I figured you might like this โœจ

add, like, all the docs

  • router docs
  • view docs
  • action docs

The rest is already documented


Building browser applications is rough. People talk about all these fancy techs but rarely provide examples on how to do things well. Even stuff like HTTP requests is often treated like: "nah mate, figure it out yourself". I would quite like to have all common cases explained, so people can create a mental model of how things work. Most of it doesn't include extending the choo api, but rather provide examples of how it can work together with the native browser APIs, and where needed provide suggestions for packages that make dealing with it easier.

Error thrown when a subscription calls 'send' immediately

Let's say I'd like to trigger an action that initialises some things first up. If I have a model like so;

app.model({
  state: {
    init: false
  },
  subscriptions: [
    send => send('init')
  ],
  reducers: {
    init: state => ({ ...state, init: true })
  }
})

The browser throws TypeError: Cannot read property 'nodeType' of null

I assume due to the view not being ready when the send occurs.

Changing the subscription function to setImmediate(() => send('init')) solves the issue, but, should this be handled by choo? Or is using subscriptions in this way not advised?

Connecting keyboard events and page transitions

I have a problem I'm trying to solve and I'm not sure the best way to solve it.

I have three pages /details/alice, /details/bill and /details/charlie, rendered from a /details/:name route. Each page has left and right arrows to flip between pages (alice -> bill -> charlie -> alice). My problem is that I also want the left and right arrow keys on the keyboard to make those transitions.

I'm trying to figure out the best way to do it with choo. All the solutions I can think of are a bit gross.

  1. I can subscribe to window.onkeydown in a subscription. That would fire a next event, in which I could look up the current page, figure out the next page and transition there. Unfortunately:
    • The router's props aren't exposed in the app's state object. So to do it I'd need to parse out the props from app:location or something.
    • The view is already figuring out the URL to transition to in its next / previous links. It feels a bit off to share that logic between model code and view code.
  2. Same as above, but I can look for the links in the page from the handler, and call .click() on them. This is what I've settled on, but its a bit weird and unwieldy. I also want to add more handlers, and needing to hook them up from two different places seems strange

Discarded options:

  1. The view can attach global window.next() and window.prev() functions, which is super gross.
  2. The view can attach global window.onkeydown event handlers. (In a sense which pages are next and previous are a property of the current view). The bad thing with this is that I would then want to remove those handlers when I transition to any other route, but without a way to have routing middleware this is difficult.

I feel like I want my route to expose a set of keyboard event handlers (this is a little game, so thats not crazy). But ... I need a way to reset them on route transition. And I'm not sure if they should live in the state object, which feels like it should be JSON.stringify()-able.

I'd love to hear some thoughts on the best way to do this sort of thing. (Sorry for abusing the issue tracker - let me know if there's a better place for questions like this.)

app.send() is not a function

In the readme the effects description implies that the app.send() method exists: https://github.com/yoshuawuyts/choo#appmodelobj

We should either revise that bit of the docs or attach send to app.

I found this writing some alternate link-catching code and wanted to send an action to update the state with the new location.

I was doing this just in an onclick event listener, but I could do it in a subscription, and that's probably the recommended approach, rather than needing app.send.

server rendering

it should work, but we probably want to do some testing first

Unmount a component

I'm wondering how best to unmount a component.
This sort of ties in with this issue #98, where a setInterval effect is calling a reducer every second.

What would be the best approach to destroy the setInterval and the component ?

requirebin

const choo = require('choo')
const app = choo()

function init (send) {
  send('start')
}

function decrement (state) {
  if (state.count === 0) {
    // implode
  } else {
    return {count: state.count - 1}
  }
}

app.model({
  state: {
    data: 'app',
    count: 5
  },
  effects: {
    start: (action, state, send) => setInterval(() => send('tick'), 1000)
  },
  reducers: {
    tick: (action, state) => decrement(state)
  },
  subscriptions: [init]
})

function main (params, state, send) {
  return choo.view`<button>${state.count}</button>`
}

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

setTimeout(function() {
  const tree = app.start({name: 'app'})
  document.body.appendChild(tree)
}, 3000)

State should be able to be an array

I have a "tables" model that's a collection of tables. I would expect to be able to set my state property to an array within the tables namespace and the resulting global state look like: { app: {}, tables: [] } as it would in redux. Instead, if you set the state to an empty array, it's not included in the global state. If you set it to an array with a value, it gets set to {tables: {0: 'that value'}}.

Not a big deal, just thought I'd point it out. At first glance I imagine it could be an issue with apply or xtend.

figure out rehydration

When an app is loaded from the server, figure out how to make it diff the nodes on first render, rather than overwrite the whole thing. Is probably just a call to yo.update.

  • write rehydration
  • update server example
  • update docs for server rendering to include rehydration

Extra:

  • write cache layer for multi routes

browser support?

Just curious, what does browser support look like for this library? Does it work everywhere out of the box? Does it require certain transforms to work in certain places? Are there old shitty versions of IE it definitely will not work in?

This would be a great thing to have in the readme as well, happy to PR it in if you want ๐Ÿ‘

subsequent render breaks google map

So at first I thought it was an issue with on-load, but I realized that Google Maps supports using an element not yet in the DOM..

Ok, so I have an example here where subsequent rerenders break the map: https://github.com/knownasilya/pizza-locator/blob/gh-pages/components/google-map.js#L13

To reproduce:

  1. visit https://knownasilya.github.io/pizza-locator/
  2. click "closest locations"

Map disappears on second render..

Looks like the element doesn't get updated properly the second time. Any thoughts?

Feature request: Programatic navigation

I've ended up using this action to navigate between pages:

  effects: {
    navigate: (action, state, send) => {
      send('app:location', {location: action.location});
      window.history.pushState({}, null, action.location);
    }
  }

It'd be nice if there was an officially recommended way to do it. (This or something else.)

Rendering a separate component at a later stage

I'm having an issue rendering a separate component (with it's own state) at a later time.

In this example, app2 is something I'd like to render at a later stage. Perhaps a user clicked a button and this component would populate a modal window.
To make things as simple as possible, this example attempts to load app2 after 3 seconds via setTimeout().

no-worky

const choo = require('choo')

// app 1
const app1 = choo()

app1.model({
  state: {
    data: 'app1',
  },
  reducers: {
    update: (action, state) => ({data: action.value})
  }
})

function app1main (params, state, send) {
  return choo.view`<button onclick=${(evt) => send('update', {value: 'app2main'})}>${state.data}</button>`
}

app1.router((route) => [
  route('/', app1main)
])

const tree1 = app1.start({name: 'app1'})
document.body.appendChild(tree1)


// app 2
// subcribes to 'init' to trigger the start of a countdown
const app2 = choo()

function init (send) {
  send('start', { payload: send })
}

app2.model({
  state: {
    data: 'app2',
    count: 5,
    timer: null
  },
  reducers: {
    update: (action, state) => ({data: action.value}),
    start: (action, state)=> {{
      timer: setInterval(() => { action.payload('tick')}, 1000)
    }},
    tick: (action, state) => ({
      count: state.count - 1
    })
  },
  subscriptions: [init]
})

function app2main (params, state, send) {
  return choo.view`<button onclick=${(evt) => send('update', {value: 'app1main'})}>${state.data} ${state.count}</button>`
}

app2.router((route) => [
  route('/', app2main)
])

setTimeout(function() {
  const tree2 = app2.start({name: 'app2'})
  document.body.appendChild(tree2)
}, 3000)

You'd notice that the 2 apps render, but nothing is really happening.
Removing the setTimeout from the rendering of app2 so they render at the same time, you'll notice app2 will start decrementing.

worky

Is there a way around this ?

choo.view -> choo/html

Changing choo.view to be require('choo/html') would make it easier to factor elements out to separate repos, relying only on bel. It would also make it easier for yo-yoify to be added (#3); and also wouldn't cause a conflict with duplicate versions of bel, as they're stripped by yo-yoify. โœจ

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?

Question: Large applications and component communication

I haven't tried this yet, only looking at the examples. But I'm interested. I'm wondering, what's the ideal way to handle this use-case within choo:

Somewhere deep in my app there's a button which, when clicked, opens a modal at the root level.

What's the expected way for the view containing the button to communicate to the root view? Do you recommend just send('root:openModal', {payload: data})?

I'd prefer my child components not have knowledge of the root component. Do you have a suggested architecture for these kind of cases?

Fetching data for a view

I have some views that depend on data from a remote source, but I can't figure out where the request(s) should happen. I'm trying to keep the HTTP code separate from my views so that requests aren't made every time the app renders. Any advice here?

First argument of start() is not optional

The docs suggest you can pass options as the first parameter to start, ie. app.start({ href: false }), but doing so throws the following errors:

Failed to execute 'appendChild' on 'Node': parameter 1 is not of type 'Node'
Uncaught TypeError: rootId.replace is not a function

It appears to be because this line begins to make the first argument optional, but this line uses the second arg as if it were the first.

Happy to submit a PR to fix this, but can't do it right this second, so just wanted to post the issue in the meantime.

Multiple choo components on a page

Just was futzing around with testing and realized that since choo controls it's domain via the addition of #choo-root on the element and the fact that everything is initialized and kept in the choo closure, it makes it impossible to have more than one choo component per page.

IMO it would be ideal if you could create multiple choo-owned objects per page, each with their own model and part of the DOM (you could add a data attribute with a randomized id and move choo into a "factory" of sorts (either through new or returning an Object.create etc)

Unbundle the http module?

Wanted to get your thoughts on whether the http module still makes sense to bundle with choo? As opposed to pointing folks to it in the readme as a "recommended" module. But if you don't need to make http requests, it isn't really relevant for you.

Keyboard example

Hey, nice work on choo!
I've just had a play with the keyboard example.

For my implementation I had to remove the ref to keyboard and rename 'app:print' to print in effects.
I am also accessing the payload value in the effect via state.

Am I chooing wrong?

const choo = require('choo')
const app = choo()

app.model({
  namespace: 'input',
  subscriptions: [
    function (send) {
      onkeypress = (e) => send('input:print', { payload: e.keyCode })
    }
  ],
  effects: {
    // state = type,payload
    // event = undefined
    print: (state, event) => console.log(`pressed key: ${state.payload}`)
  }
})

const mainView = (params, state, send) => {
  return choo.view`
    <main class="app"></main>
  `
}

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

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

docs - mention default container

in general the app.start() API is not clearly documented. I start the app and the first thing I get is

Could not find DOM node '#choo-root' to update

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

browserify transform

add yo-yoify transform so values hyperx can be stripped which saves us another 3kb; down from 7.2kb to 4.6kb

Rename "app" model namespace?

Per convo in IRC, the app model really only deals with location (the route), potentially the params, and subscriptions to hash, history, anchor links - all to do with navigating. @yoshuawuyts and I were discussing the idea of renaming the namespace to something like location, navigator, or url. Yosh pointed out location has a parallel to window.location so may be the best choice.

Thoughts?

composable / higher order effects

Sometimes multiple actions need to be chained to make a thing happen. For example: logging a user out means that:

  • local storage credentials must be wiped
  • all individual state values must be wiped
  • tokens must be invalidated on the server
  • when all that's done, navigate to a new view

redux has a concept of redux-saga to handle this. It's basically a way of combining multiple actions into a higher-order flow. Pretty damn interesting, and definitely useful. Actually I built exactly this stuff last year in the form of barracks.

So umm, I think probably the right way of dealing with this is to allow for callbacks from within effects so they can be composed into higher order effects (which we can then refer to as sagas, although they'd simply be a pattern).

An example:

const http = require('choo/http')

app.model({
  effects: {
    binbaz: (state, action, send) => {
      http.get('/foo/bar', function (err, res, body) {
        if (err) return send('oh no!', err)
        send()
      })
    },
    foobar: (state, action, send) => {
      send('binbaz', function () {
        console.log('hello world!')
        send()
      })
    }
  }
})

I feel like send() loses a bit of its semantic meaning here, perhaps an extra, optional cb() argument should be added? Anyway, I hope the idea comes across. I feel like the idea is there, but I'm overlooking quirks and the consumer-facing API isn't quite where it could be - suggestions are heaps welcome! - Cheers โœจ

See Also

improve namespaces in models

Values in state are namespaced by : instead of nesting objects. Consider nesting object instead. This would mean we'd wrap xtend by a little if a namespace exists. Namespaces should prevent the model from touching parts outside the namespace.

This has several advantages, in terms of reasoning about the model and logging too. Also makes namespaces less icky, which is great (had to explain to several people how they currently work - turned out to be tricky; simplifying is good).

Mailbox example is unable to be run in current state

Missing a requirement on tachyons and pathname-split, but even if those two are added, errorify spits out an error with Tachyons not being where it expects it to be.

This is an issue in verifying the changes in #95 for this example

Batched updates

Depending on the store/action design of an app, batching updates can be a pretty useful/important perf optimization. @yoshuawuyts what are you thoughts on the best choo-like way to handle this? Would you be interested in adding a hook that would make this possible in some app-wide way (like maybe a way to provide 'middleware' function through which updated states were passed before being emitted to the view-rendering)? Or would you prefer for people to handle this by simply throttling individual view functions?

Question: nested components

Hi @yoshuawuyts!

I was playing with the framework - it is very nice! But I couldnโ€™t figure out what is the best practice to init components declared in views.
For example, my view besides some static content has a google map container and a gallery, which init asynchronously and get dynamically updated.
Where is the best place to init them? Should I init them once view function is called?
Considering that they can asynchronously update their html content - will that conflict with the tree returned from views?
How can I manage disabling/enabling them if navigation happened?
Is there any way to indicate in views that some tags should be ignored from tree diffing, or the contrary - that only some part of the tree should be diffed?

It seems to be related to state management in yo-yo docs, where only part of view is updated. That would be nice to have something like that in choo.

What are possible approaches/workarounds to init components in views, except for webcomponents?

Thanks

Parameters swapped in documentation for effects

The documentation says effects take (state, action):

  effects: {
    'app:print': (state, action) => console.log(action.payload)
  }

But the actual call is (action, state):

      if (_effects && _effects[action.type]) {
        if (ns) _effects[action.type](action, state[ns], send)
        else _effects[action.type](action, state, send)
        effectsCalled = true
      }

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.