Security News
Input Validation Vulnerabilities Dominate MITRE's 2024 CWE Top 25 List
MITRE's 2024 CWE Top 25 highlights critical software vulnerabilities like XSS, SQL Injection, and CSRF, reflecting shifts due to a refined ranking methodology.
@arve.knudsen/choo
Advanced tools
5kb
framework for creating sturdy frontend applications
5kb
, choo
is a tiny little frameworkbrowserify
compilereffects
and subscriptions
brings
clarity to IOnote: If you've built something cool using choo
or are using it in
production, we'd love to hear from you!
Let's create an input box that changes the content of a textbox in real time. Click here to see the app running.
var html = require('choo/html')
var choo = require('choo')
var app = choo()
app.model({
state: { title: 'Not quite set yet' },
reducers: {
update: function (state, data) {
return { title: data }
}
}
})
function mainView (state, prev, send) {
return html`
<main>
<h1>Title: ${state.title}</h1>
<input type="text" oninput=${update}>
</main>
`
function update (e) {
send('update', e.target.value)
}
}
app.router(['/', mainView])
var tree = app.start()
document.body.appendChild(tree)
To run it, save it as client.js
and run with bankai. bankai
is
convenient but any browserify based tool should do:
# run and reload on port 8080
$ bankai client.js -p 8080 --open
# compile to static files in `./dist/`
$ bankai build index.js dist/
# deploy to github pages using `tschaub/gh-pages`
$ gh-pages -d dist/
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
like the current state of web development where walled gardens jealously
compete with one another. We want you to be free, not shackled to a damp
dungeon wall. By making the DOM the lowest common denominator, switching from
one framework to another becomes frictionless. Components should run anywhere
that has a DOM, regardless of the framework. 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, big dependencies, large file sizes - 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.
choo
cleanly structures internal data flow, so that all pieces of logic can
be combined into a nice, cohesive machine. Roughly speaking there are two parts
to choo
: the views and the models. Models take care of state and logic, and
views are responsible for displaying the interface and responding to user
interactions.
All of choo
's state is contained in a single object and whenever it changes
the views receive a new version of the state which they can use to safely
render a complete new representation of the DOM. The DOM is efficiently updated
using DOM diffing/patching.
The logic in choo exist in three different kinds of actions, each with their
own role: effects
, subscriptions
and reducers
.
Effects makes an asynchronous operation and calls another action when it's done.
Subscriptions (called once when the DOM loads) listens for external input like keyboard or WebSocket events and then calls another action.
Reducers receives the current state and returns an updated version of the state which is then sent to the views.
┌─────────────────┐
│ Subscriptions ─┤ User ───┐
└─ Effects ◀─────┤ ▼
┌─ Reducers ◀─────┴─────────── DOM ◀┐
│ │
└▶ Router ─────State ───▶ Views ────┘
models
are objects that contain initial state
, subscriptions
, effects
and reducers
. They're generally grouped around a theme (or domain, if you
like). To provide some sturdiness to your models
, they can either be
namespaced or not. Namespacing means that only state within the model can be
accessed. Models can still trigger actions on other models, though it's
recommended to keep that to a minimum.
So say we have a todos
namespace, an add
reducer and a todos
model.
Outside the model they're called by send('todos:add')
and
state.todos.items
. Inside the namespaced model they're called by
send('todos:add')
and state.items
. An example namespaced model:
var app = choo()
app.model({
namespace: 'todos',
state: { items: [] },
reducers: {
add: function (state, data) {
return { items: state.items.concat(data.payload) }
}
}
})
In most cases using namespaces is beneficial, as having clear boundaries makes
it easier to follow logic. But sometimes you need to call actions
that
operate over multiple domains (such as a "logout" action
), or have a
subscription
that might trigger multiple reducers
(such as a websocket
that calls a different action
based on the incoming data).
In these cases you probably want to have a model
that doesn't use namespaces,
and has access to the full application state. Try and keep the logic in these
models
to a minimum, and declare as few reducers
as possible. That way the
bulk of your logic will be safely shielded, with only a few points touching every
part of your application.
effects
are similar to reducers
except instead of modifying the state they
cause side effects
by interacting servers, databases, DOM APIs, etc. Often
they'll call a reducer when they're done to update the state. For instance, you
may have an effect called getUsers that fetches a list of users from a server
API using AJAX. Assuming the AJAX request completes successfully, the effect
can pass off the list of users to a reducer called receiveUsers which simply
updates the state with that list, separating the concerns of interacting with
an API from updating the application's state.
This is an example effect
that is called once when the application loads and
calls the 'todos:add'
reducer
when it receives data from the server:
var choo = require('choo')
var http = require('xhr')
var app = choo()
app.model({
namespace: 'todos',
state: { values: [] },
reducers: {
add: function (state, data) {
return { todos: data }
}
},
effects: {
addAndSave: function (state, data, send, done) {
var opts = { body: data.payload, json: true }
http.post('/todo', opts, function (err, res, body) {
if (err) return done(err)
data.payload.id = body.id
send('todos:add', data, function (err, value) {
if (err) return done(err)
done(null, value)
})
})
}
},
subscriptions: {
'called-once-when-the-app-loads': function (send, done) {
send('todos:addAndSave', done)
}
}
})
Subscriptions are a way of receiving data from a source. For example when
listening for events from a server using SSE
or Websockets
for a
chat app, or when catching keyboard input for a videogame.
An example subscription that logs "dog?"
every second:
var choo = require('choo')
var app = choo()
app.model({
namespace: 'app',
effects: {
print: function (state, data) {
console.log(data.payload)
}
},
subscriptions: {
callDog: function (send, done) {
setInterval(function () {
var data = { payload: 'dog?', myOtherValue: 1000 }
send('app:print', data, function (err) {
if (err) return done(err)
})
}, 1000)
}
}
})
If a subscription
runs into an error, it can call done(err)
to signal the
error to the error hook.
The router
manages which views
are rendered at any given time. It also
supports rendering a default view
if no routes match.
var app = choo()
app.router({ default: '/404' }, [
[ '/', require('./views/empty') ],
[ '/404', require('./views/error') ],
[ '/:mailbox', require('./views/mailbox'), [
[ '/:message', require('./views/email') ]
]]
])
Routes on the router
are passed in as a nested array. This means that the
entry point of the application also becomes a site map, making it easier to
figure out how views relate to each other.
Under the hood choo
uses sheet-router. Internally the
currently rendered route is kept in state.location
. To access the route
:params
you can use state.location.params
. If you want to modify
the location programmatically the effect
for the location can be called
using send('location:set', href)
. This will not work
from within namespaced models
, and usage should preferably be kept to a
minimum. Changing views all over the place tends to lead to messiness.
Views are pure functions that return a DOM tree for the router to render. They’re passed the current state, and any time the state changes they’re run again with the new state.
Views are also passed the send
function, which they can use to dispatch
actions that can update the state. For example, the DOM tree can have an
onclick
handler that dispatches an add
action.
function view (state, prev, send) {
return html`
<div>
<h1>Total todos: ${state.todos.length}</h1>
<button onclick=${addTodo}>Add</button>
</div>
`
function addTodo (e) {
send('add', { title: 'demo' })
}
}
In this example, when the Add
button is clicked, the view will dispatch an
add
action that the model’s add
reducer will receive. As seen
above, the reducer will add an item to the state’s todos
array. The
state change will cause this view to be run again with the new state, and the
resulting DOM tree will be used to efficiently patch the
DOM.
Sometimes it's necessary to change the way choo
itself works. For example to
report whenever an action is triggered, handle errors globally or perist state
somewhere. This is done through something called plugins
. Plugins are objects
that contain hook
and wrap
functions and are passed to app.use()
:
var log = require('choo-log')
var choo = require('choo')
var app = choo()
app.use(log())
var tree = app.start()
document.body.appendChild(tree)
Generally people using choo
shouldn't be too worried about the specifics of
plugins
, as the internal API is (unfortunately by necessity) quite complex.
After all they're the most powerful way to modify a choo
application.
:warning: Warning :warning:: plugins should only be used as a last resort. It creates peer dependencies which makes upgrading versions and switching frameworks a lot harder. Please exhaust all other options before using plugins.
If you want to learn more about creating your own plugins
, and which hooks
and wrappers
are available, head on over to app.use().
Using choo
in a project? Show off which version you've used using a badge:
[![built with choo v4](https://img.shields.io/badge/built%20with%20choo-v4-ffc3e4.svg?style=flat-square)](https://github.com/yoshuawuyts/choo)
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 or
concepts first :sparkles:
Initialize a new choo
app. Takes an optional object of handlers that is
passed to app.use().
Opts can also contain the following values:
true
. Enable a subscription
to the browser
history API. e.g. updates the internal location.href
state whenever the
browsers "forward" and "backward" buttons are pressed.true
. Handle all relative <a href="<location>"></a>
clicks and update internal state.location
accordingly.false
. Enable a subscription
to the hash change
event, updating the internal state.location
state whenever the URL hash
changes (eg localhost/#posts/123
). Enabling this option automatically
disables opts.history
and opts.href
.Create a new model. Models modify data and perform IO. Takes the following arguments:
state
inside the modelactions
. Signature of (state, data)
.actions
, can call actions
. Signature of (state, data, send, done)
actions
. Signature of (send, done)
.Send a new action to the models with optional data attached. Namespaced models
can be accessed by prefixing the name with the namespace separated with a :
,
e.g. namespace:name
.
When sending data from inside a model
it expects exactly three arguments: the name of the action you're calling, the data you want to send, and finally a callback to handle errors through the global onError()
hook. So if you want to send two values, you'd have to either send an array or object containing them.
When an effect
or subscription
is done executing, or encounters an error,
it should call the final done(err, res)
callback. If an effect
was called
by another effect
it will call the callback of the caller. When an error
propegates all the way to the top, the onError
handler will be called,
registered in choo(handlers)
. If no callback is registered, errors will
throw
.
Creates a new router. Takes a function that exposes a single route
function,
and that expects a tree of routes
to be returned. See sheet-router for full
documentation. Registered views have a signature of (state, prev, send)
,
where state
is the current state
, prev
is the last state,
state.location.params
is URI partials and send()
can be called to trigger
actions. If defaultRoute
is passed in, that will be called if no paths match.
If no defaultRoute
is specified it will throw instead.
Register an object of hooks on the application. This is useful to extend the
way choo
works, adding custom behavior and listeners. Generally returning
objects of hooks
is done by returning them from functions (which we call
plugins
throughout the documentation).
There are several hooks
and wrappers
that are picked up by choo
:
effect
or
subscription
emit an error. If no handler is passed, the default handler
will throw
on each error.action
is fired.state
.subscription
to add custom behaviorreducer
to add custom behavioreffect
to add custom behaviorstate
to add custom
behavior - useful to mutate the state before starting up:warning: Warning :warning:: plugins should only be used as a last resort. It creates peer dependencies which makes upgrading versions and switching frameworks a lot harder. Please exhaust all other options before using plugins.
createSend()
is a special function that allows the creation of a new named
send()
function. The first argument should be a string which is the name, the
second argument is a boolean callOnError
which can be set to true
to call
the onError
hook istead of a provided callback. It then returns a
send(actionName, data?)
function.
Hooks should be used with care, as they're the most powerful interface into
the state. For application level code it's generally recommended to delegate to
actions inside models using the send()
call, and only shape the actions
inside the hooks.
Render the application to a string of HTML. Useful for rendering on the server.
First argument is a path that's passed to the router. Second argument is an
optional state object. When calling .toString()
instead of .start()
, all
calls to send()
are disabled, and subscriptions
, effects
and reducers
aren't loaded.
Start the application. Returns a tree of DOM nodes that can be mounted using
document.body.appendChild()
.
Tagged template string HTML builder. Built on top of yo-yo, bel, and
hyperx. To register a view on the router
it should be wrapped in a function
with the signature of (state, prev, send)
where state
is the current
state
, prev
is the last state, state.location.params
is URI partials and
send()
can be called to trigger actions.
To create listeners for events, create interpolated attributes on elements.
var html = require('choo/html')
html`
<button onclick=${log}>click for bananas</button>
`
function log (e) {
console.log(e)
}
Example listeners include: onclick
, onsubmit
, oninput
, onkeydown
,
onkeyup
. A full list can be found at the yo-yo
repo. When
creating listeners always remember to call e.preventDefault()
and
e.stopPropagation()
on the event so it doesn't bubble up and do stuff like
refreshing the full page or the like.
To trigger lifecycle events on any part of a view, set the onload=${(el) => {}}
and onunload=${(el) => {}}
attributes. These parameters are useful when
creating self-contained widgets that take care of their own state and lifecycle
(e.g. a maps widget) or to trigger animations. Most elements shouldn't have a
need for these hooks though.
Use choo/mount
to mount a tree of DOMNodes at a given selector. This is
especially useful to mount a <body>
tag on the document body after rendering
from the server. It makes sure all <script>
tags and similar are persisted so
no duplicate download calls are triggered.
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 to the company using choo
.
:steam_locomotive::train::train::train:
I love small libraries that do one thing well, but when working in a team,
having an undocumented combination of packages often isn't great. choo()
is a
small set of packages that work well together, wrapped in an an architectural
pattern. This means you get all the benefits of small packages, but get to be
productive right from the start without needing to plough through layers of
boilerplate.
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.
Ah, so this is where I get to rant. choo
(chugga-chugga-chugga-choo-choo!)
was built because other options didn't quite cut it for me, so instead of
presenting some faux-objective chart with skewed benchmarks and checklists I'll
give you my opinions directly. Ready? Here goes:
react
is kind of big (155kb
was it?). They also
like classes a lot, and enforce a lot of abstractions. It also encourages
the use of JSX
and babel
which break JavaScript, The Language™. And all
that without making clear how code should flow, which is crucial in a team
setting. I don't like complicated things and in my view react
is one of
them. react
is not for me.react
. However it doesn't fix the large dependencies react
seems to use
(e.g. react-router
and friends) and doesn't help at all with architecture.
If react
is your jam, and you will not budge, sitting at 3kb
this is
probably a welcome gift.angular
doesn't tick any box in my book of nice things.TypeScript
and RxJS
definitely hasn't made things simpler. Last I checked
it was ~200kb
in size before including some monstrous extra deps. I guess
angular
and I will just never get along.mercury
is an interesting one. It seemed like a brilliant
idea until I started using it - the abstractions felt heavy, and it took team
members a long time to pick up. In the end I think using mercury
helped
shaped choo
greatly, despite not working out for me.deku
is fun. I even contributed a bit in the early days. It could
probably best be described as "a functional version of react
". The
dependence on JSX
isn't great, but give it a shot if you think it looks
neat.cycle
's pretty good - unlike most frameworks it lays out a clear
architecture which helps with reasoning about it. That said, it's built on
virtual-dom
and xstream
which are a bit heavy for my taste. choo
works
pretty well for FRP style programming, but something like inu might be
an interesting alternative.cycle
, vue
is pretty good. But it also uses tech that
provides framework lock in, and additionally doesn't have a clean enough
architecture. I appreciate what it does, but don't think it's the answer.In Node, reducers
, effects
and subscriptions
are disabled for performance
reasons, so if send()
was called to trigger an action it wouldn't work. Try
finding where in the DOM tree send()
is called, and disable it when called
from within Node.
choo
uses morphdom, 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.
choo
really shines when coupled with browserify
transforms. They can do
things like reduce file size, prune dependencies and clean up boilerplate code.
Consider running some of the following:
assert()
statements which reduces file size. Use as a --global
transformconst
,
arrow functions
and template strings
to older browsers. Should be run as
a --global
transformhyperx
dependency with document.createElement
calls; greatly speeds up performance
too--global
transformprocess.env
values
with plain stringsOut of the box choo
only supports runtimes which support:
const
arrow functions
(e.g. () => {}
)template strings
This does not include Safari 9 or any version of IE. If support for these platforms is required you will have to provide some sort of transform that makes this functionality available in older browsers. The test suite uses es2020 as a global transform, but anything else which might satisfy this requirement is fair game.
Generally for production builds you'll want to run:
$ NODE_ENV=production browserify \
-t envify \
-g yo-yoify \
-g unassertify \
-g es2020 \
-g uglifyify \
| uglifyjs
Yup, it's greatly inspired by the elm
architecture. But contrary to elm
,
choo
doesn't introduce a completely new language to build web applications.
Sure.
$ npm install choo
choo
guidebrowserify
browserify
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.
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
Become a backer, and buy us a coffee (or perhaps lunch?) every month or so. Become a backer
FAQs
A 5kb framework for creating sturdy frontend applications
The npm package @arve.knudsen/choo receives a total of 3 weekly downloads. As such, @arve.knudsen/choo popularity was classified as not popular.
We found that @arve.knudsen/choo demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
MITRE's 2024 CWE Top 25 highlights critical software vulnerabilities like XSS, SQL Injection, and CSRF, reflecting shifts due to a refined ranking methodology.
Security News
In this segment of the Risky Business podcast, Feross Aboukhadijeh and Patrick Gray discuss the challenges of tracking malware discovered in open source softare.
Research
Security News
A threat actor's playbook for exploiting the npm ecosystem was exposed on the dark web, detailing how to build a blockchain-powered botnet.